AXI4 UVM (3) — axi4_driver

Seungyun Lee·2026년 7월 30일

AXI4_UVM_FULL

목록 보기
6/16

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


axi4_driver — 파이프라인 마스터 드라이버

전체 구조

              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  ← 응답 수집 + 카운터 감소

왜 이 구조인가 — Blocking과 비교

❌ 전형적인 Blocking 드라이버

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는 데이터만 읽어 충돌 없음

Inflight & 계측 카운터

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 → "진짜 파이프라인 동작" 증거!
→ 검증 환경 자체를 검증하는 계측 (인터뷰 어필 포인트)

is_idle() — Drain 판정

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  → 버스에서 진행 중인 게 없음

run_phase() & reset_signals()

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 ... join6개 스레드 기동

reset_signals() — 페이로드가 리셋 안 되는 이슈

리셋되는 것:  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; 등 명시적 초기화

item_thread() — 파이프라인의 심장

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(경쟁 상태)이라는 치명적인 버그를 막기 위해서입니다.

Throttle — Outstanding depth 제한

max_outstanding=4:
  n_inflight 0→1→2→3→4 통과, 4에서 블록 (4>=4)
  B 응답 → n_inflight=3 → 통과

throttle이 get_next_item 뒤에 있는 이유:
  대기 중 item_done() 안 불림 → 시퀀스도 블록
  → 백프레셔가 시퀀스까지 자연스럽게 전달 ✅

aw_q와 w_q에 같은 순서로 push

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

aw_thread() / ar_thread() — 주소 채널

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 감지 = 핸드셰이크 성립 ✅

연속 AW 발행 시 거동

핸드셰이크 후:
  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채널)보다 단순.


w_thread() — 쓰기 데이터 채널

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

WLAST 생성

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)

⚠️ 큰 이슈 — W가 AW보다 먼저 나갈 수 있음

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 버퍼링

b_thread() / r_thread() — 응답 채널

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_thread vs r_thread 차이

쓰기: 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 자동 발생

inflight & max_outstanding — 용어 심화

두 개념 모두 "지금 처리 중인 트랜잭션"과 관련되지만 미묘하게 다르다.

inflight 뜻

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개)"

max_outstanding 뜻

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 vs wr_outstanding — 시점이 다름

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 = 파이프라인 증거

Throttle 메커니즘 (max_outstanding 제한)

aw_thread 핸드셰이크 상세 (골든 룰)

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

Write 버스트 생명주기 시퀀스

전체 마인드맵


profile
Design Verification engineer

0개의 댓글