시리즈: 아이템 · 인터페이스 · 드라이버 · 모니터
Sequencer
│ get_next_item()
↓
┌── item_thread ──┐ ← item_done()을 "즉시" 호출
│ │ (파이프라인의 핵심!)
↓ ↓
[aw_q][w_q] [ar_q]
│ │ │
↓ ↓ ↓
aw_thread w_thread ar_thread ← 각 채널 독립 드라이브
│ │ │
└────┴──── AXI 버스 ┴────┐
│ │
b_thread r_thread ← 응답 수집 + 카운터 감소
forever begin
seq_item_port.get_next_item(req);
drive_transaction(req); // AW → W → B 전부 대기
seq_item_port.item_done(); // 버스에서 끝난 후에야 호출
end
→ 버스가 놀아도 다음 트랜잭션이 안 들어옴
→ Outstanding 절대 발생 안 함
→ 슬레이브의 Outstanding 지원 로직이 검증 안 됨!
task item_thread();
forever begin
seq_item_port.get_next_item(req);
aw_q.push_back(req);
w_q.push_back(req);
n_inflight++;
seq_item_port.item_done(); // ← 즉시 호출!
end
endtask
→ 시퀀스가 버스보다 앞서 달림 → 버스에 여러 버스트 동시 존재 ✅
item_done()을 "언제" 부르느냐가 blocking과 pipelined를 가르는 유일한 차이
max_outstanding = 1 → blocking 드라이버와 동일
max_outstanding = >1 → pipelined
→ 같은 코드로 두 모드 커버, 회귀에서 둘 다 검증 가능
Outstanding 미지원 슬레이브:
AWREADY를 낮게 유지 → aw_thread가 while에서 자동 블록
→ 드라이버 수정 없이 자동 적응 (graceful degradation)
class axi4_driver extends uvm_driver #(axi4_seq_item);
uvm_driver #(T) 상속 시 자동 생성:
T req; ← 요청 아이템 (built-in)
uvm_seq_item_pull_port #(T) seq_item_port; ← 시퀀서 연결
protected axi4_seq_item aw_q[$]; // AW 발행 대기
protected axi4_seq_item w_q[$]; // W beat 전송 대기
protected axi4_seq_item ar_q[$]; // AR 발행 대기
모니터 큐와 방향 정반대:
모니터: 관찰한 것을 쌓아 완성되면 발행
드라이버: 보낼 것을 쌓아 하나씩 꺼내 구동
같은 트랜잭션이 aw_q, w_q에 동시에 들어감 (Handle 2개, Object 1개)
→ aw_thread는 주소만, w_thread는 데이터만 읽어 충돌 없음
protected int unsigned n_inflight; // 시퀀서 기준 진행 중
int unsigned wr_outstanding, wr_peak; // 버스 기준 진행 중
int unsigned rd_outstanding, rd_peak;
n_inflight = "시퀀서에서 받은 것" (큐 포함)
wr_outstanding = "버스에서 진행 중" (AW 수락 ~ B 응답)
차이가 중요:
큐에 10개 쌓여도 슬레이브가 AWREADY 안 올리면
n_inflight=10, wr_outstanding=1
wr_peak >= 2 → "진짜 파이프라인 동작" 증거!
→ 검증 환경 자체를 검증하는 계측 (인터뷰 어필 포인트)
function bit is_idle();
return (aw_q.size() == 0) && (w_q.size() == 0) &&
(ar_q.size() == 0) && (n_inflight == 0);
endfunction
문제: 파이프라인 드라이버는 마지막 item_done() 후에도
버스에 버스트가 남아있음 → 그냥 끝내면 응답 유실!
해결 (테스트에서):
seq.start(sequencer);
wait (env.agent.driver.is_idle()); // 드레인 대기
phase.drop_objection(this);
4개 조건 전부 체크:
큐(3개) == 0 → 대기 중인 게 없음
n_inflight == 0 → 버스에서 진행 중인 게 없음
task run_phase(uvm_phase phase);
reset_signals();
@(negedge vif.rst);
@(vif.master_cb); // 한 클록 더 대기
fork
item_thread(); aw_thread(); w_thread();
b_thread(); ar_thread(); r_thread();
join
endtask
| 단계 | 이유 |
|---|---|
reset_signals() | 리셋 중 신호를 안전한 값으로 |
@(negedge vif.rst) | 리셋 해제 대기 |
@(vif.master_cb) | 리셋 해제가 클록과 무관한 시점일 수 있어 클록에 정렬 |
fork ... join | 6개 스레드 기동 |
리셋되는 것: awvalid/wvalid/arvalid (VALID 계열),
bready/rready (READY 계열),
awlock/awcache/awprot 등 사이드밴드
리셋 안 되는 것: awid/awaddr/awlen/awsize/awburst,
wdata/wstrb, arid/araddr/... → 첫 트랜잭션 전까지 X!
AXI상 합법 (VALID=0일 때 페이로드 don't-care)
하지만 실무: 파형 X 가득 → 디버깅 어려움, X 전파 위험
→ "X-state propagation guard"와 같은 맥락
→ 개선: awaddr <= '0; wdata <= '0; 등 명시적 초기화
task item_thread();
forever begin
seq_item_port.get_next_item(req);
while (n_inflight >= cfg.max_outstanding) @(vif.master_cb); // throttle
if (req.dir == AXI_WRITE) begin
aw_q.push_back(req);
w_q.push_back(req); // same order -> AXI write-data order
end
else ar_q.push_back(req);
n_inflight++;
seq_item_port.item_done(); // 즉시!
end
endtask
while (n_inflight >= cfg.max_outstanding) @(vif.master_cb); // throttle
진행 중인 트랜잭션(In-flight) 개수가 허용치(Max Outstanding)에 도달하면, 빈자리가 날 때까지 클락을 기다려라
@(vif.master_cb);의 실제 의미
@ : 기다려라 (Wait for event)
vif : 저기 연결된 인터페이스를 타고 들어가서
.master_cb : 거기에 정의된 클락킹 블록을 참조해라.
// 하드웨어와 연결되는 실제 인터페이스 정의
interface axi_if(input logic clk, input logic rst_n);
// Interface
// 신호들 선언
logic [31:0] awaddr;
logic awvalid;
logic awready;
// ...
// 클락킹 블록 선언: "나는 clk의 상승 엣지(posedge)를 기준으로 동작하겠다"
clocking master_cb @(posedge clk);
default input #1step output #1ns; // 셋업/홀드 타임 정의 (Race Condition 방지)
output awaddr, awvalid; // 내가(Master가) 쏠 신호들
input awready; // 내가(Master가) 읽을 신호들
endclocking
위의 인터페이스 코드에서 master_cb 옆에 @(posedge clk)라고 기준을 명시해 두었죠?
따라서 @(vif.master_cb);는 컴파일러가 볼 때 사실상 @(posedge clk);과 똑같이 동작하게 됩니다. 즉, "이 클락킹 블록의 기준이 되는 클락 엣지(상승 엣지)가 한 번 칠 때까지 멈춰서 기다려라"라는 의미가 됩니다.
그렇다면 왜 굳이 복잡하게 master_cb를 쓸까?
그냥 직관적으로 @(posedge vif.clk); 라고 쓰면 편할 텐데, 굳이 클락킹 블록을 참조하는 이유는 Race Condition(경쟁 상태)이라는 치명적인 버그를 막기 위해서입니다.
max_outstanding=4:
n_inflight 0→1→2→3→4 통과, 4에서 블록 (4>=4)
B 응답 → n_inflight=3 → 통과
throttle이 get_next_item 뒤에 있는 이유:
대기 중 item_done() 안 불림 → 시퀀스도 블록
→ 백프레셔가 시퀀스까지 자연스럽게 전달 ✅
AXI4에 WID 없음 → W beat는 AW와 같은 순서로 나가야 함
두 큐 같은 순서 채우고 둘 다 FIFO 소비 → 순서 보장 ✅
// 위험한 시퀀스
axi4_seq_item tr = create("tr"); // 한 번만 생성!
repeat(10) begin
start_item(tr);
tr.randomize(); // 같은 객체 다시 랜덤화!
finish_item(tr);
end
드라이버 큐:
1회차: aw_q=[tr] tr.addr=0x1000
2회차: tr.randomize() → tr.addr=0x2000 ← 큐 안의 것도 바뀜!
aw_q=[tr,tr] 둘 다 0x2000!
→ "Queue에 Object 넣고 원본 수정" 문제 그대로
→ Blocking이면 안 생김, 파이프라인에서만 터지는 버그!
해결:
- 시퀀스에서 매번 create (권장)
- 또는 드라이버에서 req.clone()으로 deep copy
task aw_thread();
axi4_seq_item tr;
forever begin
while (aw_q.size() == 0) @(vif.master_cb); // 폴링 대기
tr = aw_q.pop_front();
vif.master_cb.awid <= tr.id;
vif.master_cb.awaddr <= tr.addr;
vif.master_cb.awlen <= tr.len;
vif.master_cb.awsize <= tr.size;
vif.master_cb.awburst <= tr.burst;
vif.master_cb.awvalid <= 1'b1; // ① VALID 올림
@(vif.master_cb); // ② 한 클록
while (!vif.master_cb.awready) @(...); // ③ READY 대기 (VALID 유지)
vif.master_cb.awvalid <= 1'b0; // ④ 완료 후 내림
wr_outstanding++;
if (wr_outstanding > wr_peak) wr_peak = wr_outstanding;
end
endtask
룰 1 (Deadlock 방지): ①에서 awready 확인 없이 바로 VALID 올림 ✅
룰 2 (VALID 유지): ③의 while 동안 awvalid=1 유지 ✅
룰 4 (전송 조건): ③의 awready 감지 = 핸드셰이크 성립 ✅
핸드셰이크 후:
awvalid <= 0 (T=21ns 예약)
loop → pop → awvalid <= 1 (T=21ns 예약, 같은 시각!)
→ NBA 규칙: 나중 할당 승리 → awvalid=1 유지
→ back-to-back AW (AXI상 정상) ✅
wr_outstanding++ : AW 핸드셰이크 성립 후 (버스에 나갔을 때)
wr_outstanding-- : BVALID 수신 (b_thread)
n_inflight와 시점 다름 → 두 카운터 차이 = 큐 대기 개수
ar_thread는 aw_thread와 구조 완전 동일. 읽기는 데이터를 보낼 필요가 없어
(AR + R, 2채널) 쓰기(AW + W + B, 3채널)보다 단순.
task w_thread();
axi4_seq_item tr;
forever begin
while (w_q.size() == 0) @(vif.master_cb);
tr = w_q.pop_front();
foreach (tr.data[i]) begin
vif.master_cb.wdata <= tr.data[i];
vif.master_cb.wstrb <= tr.strb[i];
vif.master_cb.wlast <= (i == tr.len); // 마지막 beat
vif.master_cb.wvalid <= 1'b1;
@(vif.master_cb);
while (!vif.master_cb.wready) @(vif.master_cb);
vif.master_cb.wvalid <= 1'b0;
vif.master_cb.wlast <= 1'b0;
end
end
endtask
vif.master_cb.wlast <= (i == tr.len);
len=3: i=0→0, i=1→0, i=2→0, i=3→1 (마지막만 1) ✅
wlast는 매 beat 후 0으로 내림 → 안 내리면 다음 버스트 첫 beat에
wlast=1 → 프로토콜 위반!
⚠️ 방어 부족: data.size()가 len+1이 아니면 WLAST 위치 틀림
더 안전: (i == tr.data.size()-1)
aw_thread와 w_thread가 완전 독립:
슬레이브가 AWREADY를 오래 낮게 유지 + WREADY는 높음
→ aw_thread: AW #1 대기 (블록)
→ w_thread: W beats #1, #2 전부 전송!
→ AW #1도 안 나갔는데 W #2 데이터가 나감
AXI상 합법이지만 이 모니터와 충돌:
mon_w: aw_q.size()==0 → "W beat with no AW - ignored" → beat 유실!
→ 개선: aw_thread에서 핸드셰이크 후 w_q에 push
또는 모니터가 W-before-AW 버퍼링
task b_thread();
vif.master_cb.bready <= 1'b1; // always ready
forever begin
@(vif.master_cb);
if (vif.master_cb.bvalid) begin
if (n_inflight > 0) n_inflight--;
if (wr_outstanding > 0) wr_outstanding--;
end
end
endtask
task r_thread();
vif.master_cb.rready <= 1'b1;
forever begin
@(vif.master_cb);
if (vif.master_cb.rvalid && vif.master_cb.rlast) begin // RLAST!
if (n_inflight > 0) n_inflight--;
if (rd_outstanding > 0) rd_outstanding--;
end
end
endtask
쓰기: B 응답은 버스트당 1개 → bvalid만 체크
읽기: R beat는 여러 개 → RLAST로 버스트 끝 판단!
len=3 읽기: rlast=0,0,0,1 → 마지막에만 카운터--
✅ always ready (bready/rready=1) → 응답 지연 안 시킴, 코드 단순
✅ > 0 가드 → int unsigned 언더플로 방지 (0에서 -- → 거대한 수)
⚠️ BRESP/RDATA 안 읽음 → item_done() 이미 호출해서 시퀀스에 못 돌려줌
→ 검증은 Monitor+Scoreboard 담당 (일관된 철학)
⚠️ backpressure 미검증 → READY 항상 1이라 마스터 지연 시나리오 없음
→ 개선: 랜덤 bready/rready 지연 주입
function void report_phase(uvm_phase phase);
`uvm_info("DRV", $sformatf(
"peak writes = %0d, peak reads = %0d (max_outstanding = %0d)",
wr_peak, rd_peak, cfg.max_outstanding), UVM_LOW)
endfunction
출력: peak writes = 4 → 파이프라인 동작 ✅
peak writes = 1 → 파이프라인 안 됨 (backpressure or 트래픽 부족)
개선: max_outstanding>1인데 peak<=1이면 uvm_warning 자동 발생
두 개념 모두 "지금 처리 중인 트랜잭션"과 관련되지만 미묘하게 다르다.
in flight = "비행 중" = "출발했지만 아직 착륙(완료) 안 한"
트랜잭션이 "출발했지만 아직 응답을 못 받은 상태"
택배 비유:
주문함 → [inflight: 배송 중!] → 도착 완료
n_inflight = 지금 배송 트럭에 실려 오는 택배 개수
protected int unsigned n_inflight;
// ++ : item_thread에서 큐에 넣을 때 (출발!)
// -- : b_thread(BVALID) 또는 r_thread(RLAST)에서 (도착!)
시퀀서 → [큐] → AXI 버스 → 응답
└──── 이 구간에 있는 것들이 inflight ────┘
n_inflight = 3 → "지금 3개 버스트가 처리 중 (응답 안 온 것 3개)"
outstanding = "미결/미처리된" (금융의 "미상환 대출"과 같은 뉘앙스)
max_outstanding = "동시에 진행 중일 수 있는 최대 개수" (설정값)
while (n_inflight >= cfg.max_outstanding) @(vif.master_cb);
// cfg = agent config 객체, max_outstanding = 그 안의 설정값
// "inflight가 max에 도달하면 더 안 보내고 대기"
max_outstanding = 주차장 전체 자리 수 (예: 4칸, 고정)
n_inflight = 지금 주차된 차 수 (0~4, 변동)
n_inflight ≤ max_outstanding ← 항상 유지
4칸 다 차면 → 나가는 차(응답) 있을 때까지 새 차 대기
동작:
n_inflight 0→1→2→3→4 통과, 4에서 블록 (4>=4)
B 응답 도착 → n_inflight=3 → 다시 통과
max_outstanding = 1 → 한 번에 하나씩 = blocking 드라이버
max_outstanding = 4 → 최대 4개 동시 = pipelined
→ 같은 드라이버 코드로 설정값만 바꿔
"직렬 모드"와 "파이프라인 모드"를 둘 다 테스트!
n_inflight : 시퀀서에서 받는 순간 ++ (큐에 있을 때부터)
wr_outstanding : AW가 버스에서 수락된 순간 ++ (실제 버스에 나갔을 때)
큐에 5개 쌓였는데 슬레이브가 AWREADY를 안 올리면:
n_inflight = 5 (받긴 다 받음)
wr_outstanding = 1 (실제 버스엔 하나만)
→ 차이 = 큐에서 대기 중인 개수
→ wr_peak >= 2 가 "진짜 파이프라인 동작" 증거

item_thread가 큐에 넣고 item_done()을 즉시 부르기 때문에,
시퀀스가 버스보다 앞서 달려서 여러 버스트가 동시에 진행된다.

n_inflight : 큐에 있을 때부터 셈 (시퀀서 기준)
wr/rd_outstanding : 실제 버스에 나갔을 때부터 셈 (버스 기준)
차이 = 큐 대기 개수, peak >= 2 = 파이프라인 증거


룰 1 (Deadlock 방지) : ①에서 awready 확인 없이 바로 VALID 올림
룰 2 (VALID 유지) : ③의 while 동안 awvalid=1 유지
룰 4 (전송 조건) : ③의 awready 감지 = 핸드셰이크 성립

