AXI4 UVM (10) — sequences

Seungyun Lee·2026년 7월 30일

AXI4_UVM_FULL

목록 보기
13/16

시리즈: ... · agent_cfg/agent/env · base_test · sequences


Sequence란

"어떤 트랜잭션을 어떤 순서로 만들지"를 정의하는 자극 프로그램

test가 get_seq()로 고르는 그 시퀀스들
→ 실제로 아이템(axi4_seq_item)을 randomize해서 시퀀서에 밀어넣음

지금까지: test가 "어떤 시퀀스 쓸지" 골랐다면, 이 파일은 그 시퀀스가 실제로 뭘 하는지다.


전체 구조

axi4_base_seq (부모)
    ├── num 필드 (몇 개 만들지)
    └── wr_then_rd() 헬퍼 (쓰고 같은 영역 읽기)

    ↓ 상속

axi4_write_seq    : N개 랜덤 쓰기
axi4_read_seq     : N개 랜덤 읽기
axi4_rand_seq     : N개 랜덤 (읽기/쓰기 섞임)
axi4_wr_rd_seq    : 쓰고 → 같은 영역 읽기 (self-checking, 기본 시퀀스)
axi4_os_*_seq     : outstanding용 (고정 주소 8)
axi4_cov_seq      : 커버리지 채우기 (모든 조합 sweep)
axi4_directed_seq : 손으로 고른 코너 케이스
axi4_wrap_seq     : WRAP 버그 재현 (FAIL 예상)

⭐ 가장 중요한 설계 결정 — WRAP 격리

// the general-purpose sequences deliberately exclude AXI_WRAP so
// the normal regression is clean.
일반 시퀀스들은 일부러 AXI_WRAP을 제외함!

이유:
  DUT가 WRAP을 선형 증가로 처리하는 버그가 있음
  → 일반 시퀀스에 WRAP을 넣으면 정상 회귀도 FAIL
  → WRAP은 별도 시퀀스/테스트로 격리해서 "일부러" 버그 시연

→ 그래서 대부분 시퀀스에 burst inside {AXI_INCR, AXI_FIXED}
  (WRAP 빠짐)

이건 실무에서 중요한 판단이에요. "알려진 버그"를 정상 회귀에서 분리해서, 회귀는 clean하게 유지하고 버그는 별도로 문서화/추적하는 방식.


Part 1: axi4_base_seq — 부모 & 헬퍼

class axi4_base_seq extends uvm_sequence #(axi4_seq_item);
    `uvm_object_utils(axi4_base_seq)
    rand int unsigned num = 5;   // how many transactions / pairs

    function new(string name = "axi4_base_seq");
        super.new(name);
    endfunction
uvm_sequence #(axi4_seq_item) 상속
  → axi4_seq_item 타입 아이템을 생성/전달하는 시퀀스

num = 생성할 트랜잭션 개수 (기본 5)
  → test에서 s.num = 40 처럼 바꿀 수 있음 (cov_test에서 봤던 것)

wr_then_rd() 헬퍼 — 핵심 도구

task wr_then_rd(bit [AXI_ADDR_WIDTH-1:0] a,
                bit [7:0]                l,
                bit [2:0]                s,
                axi_burst_e              b);
    axi4_seq_item wr, rd;

    wr = axi4_seq_item::type_id::create("wr");
    start_item(wr);
    if (!wr.randomize() with { dir == AXI_WRITE; addr == a;
                               len == l; size == s; burst == b; })
        `uvm_error("RAND", "directed write randomize() failed")
    finish_item(wr);

    rd = axi4_seq_item::type_id::create("rd");
    start_item(rd);
    if (!rd.randomize() with { dir == AXI_READ; addr == a;
                               len == l; size == s; burst == b;
                               id == wr.id; })
        `uvm_error("RAND", "directed read randomize() failed")
    finish_item(rd);
endtask
"쓰고 → 정확히 같은 영역 읽기" 한 세트

같은 addr/len/size/burst로:
  1. 쓰기 아이템 생성 → 랜덤 데이터로 씀
  2. 읽기 아이템 생성 → 같은 영역 읽음
  → 스코어보드가 "쓴 값 == 읽은 값" 비교 → self-checking

정상 DUT면 데이터 일치해야 함

start_item / finish_item — 시퀀스-드라이버 핸드셰이크

start_item(wr)"이 아이템 보낼게, 시퀀서 준비되면 알려줘"
                   (드라이버가 get_next_item 할 때까지 대기)
wr.randomize()   → 제약 안에서 랜덤화
finish_item(wr)"완성됐어, 드라이버로 보내"
                   (드라이버가 item_done 할 때까지 대기)

→ driver의 get_next_item() / item_done()과 짝을 이루는 반대편!

randomize() with { ... } — 인라인 제약

randomize() with { dir == AXI_WRITE; addr == a; ... }

= "이번 랜덤화는 이 조건들을 지켜라"
  dir는 WRITE로 고정
  addr는 인자 a로 고정
  나머지(data, strb)는 seq_item 제약 안에서 랜덤

→ seq_item의 constraint(c_4k, c_align 등)는 그대로 적용되면서
  추가로 이 조건들을 얹음

읽기가 id == wr.id 하는 이유

읽기 아이템의 id를 쓰기와 같게 맞춤
→ 같은 트랜잭션 쌍임을 ID로 표시
→ (outstanding 환경에서 매칭에 도움)

Part 2: 기본 랜덤 시퀀스들

write / read / rand — 거의 동일

class axi4_write_seq extends axi4_base_seq;
    task body();
        repeat (num) begin
            req = axi4_seq_item::type_id::create("req");
            start_item(req);
            if (!req.randomize() with { dir == AXI_WRITE;
                                        burst inside {AXI_INCR, AXI_FIXED}; })
                `uvm_error("RAND", "write item randomize() failed")
            finish_item(req);
        end
    endtask
endclass
body() = 시퀀스의 메인 (test가 seq.start() 하면 실행됨)

repeat (num) → num개 생성
req = built-in 핸들 (uvm_sequence가 제공)

세 시퀀스 차이는 dir 제약만:
  write_seq: dir == AXI_WRITE
  read_seq:  dir == AXI_READ
  rand_seq:  dir 제약 없음 (읽기/쓰기 랜덤 혼합)

공통: burst inside {AXI_INCR, AXI_FIXED}  ← WRAP 제외!

wr_rd_seq — 기본 시퀀스 (base_test가 씀)

class axi4_wr_rd_seq extends axi4_base_seq;
    task body();
        repeat (num) begin
            axi4_seq_item wr, rd;

            wr = axi4_seq_item::type_id::create("wr");
            start_item(wr);
            if (!wr.randomize() with { dir == AXI_WRITE; burst == AXI_INCR; })
                `uvm_error("RAND", "write randomize() failed")
            finish_item(wr);

            rd = axi4_seq_item::type_id::create("rd");
            start_item(rd);
            if (!rd.randomize() with { dir == AXI_READ; burst == AXI_INCR;
                                       addr == wr.addr; len == wr.len;
                                       size == wr.size; id == wr.id; })
                `uvm_error("RAND", "read randomize() failed")
            finish_item(rd);
        end
    endtask
endclass
랜덤 쓰기 → 같은 영역 읽기 (wr_then_rd와 비슷하지만 주소도 랜덤)

wr는 완전 랜덤 (addr/len/size 랜덤)
rd는 wr를 따라감 (addr == wr.addr, len == wr.len, ...)
→ 랜덤한 위치에 쓰고 그 위치를 읽어서 검증

이게 base_test의 기본 자극 → "환경이 제대로 도나" 확인


Part 3: outstanding 시퀀스

고정 주소 계획

class axi4_os_base_seq extends axi4_base_seq;
    localparam int OS_COUNT = 8;
    // 0x5000, 0x5100, ... 0x5700
    function bit [AXI_ADDR_WIDTH-1:0] os_addr(int i);
        return 16'h5000 + (i * 16'h0100);
    endfunction
endclass
8개 버스트를 고정된 겹치지 않는 주소에 배치
  0x5000, 0x5100, 0x5200, ... 0x5700
  각 4 beat × 4B, 전부 4KB 안전

왜 고정 주소? outstanding은 "여러 개 동시 진행"을 봐야 하는데
            주소가 겹치면 검증이 복잡 → 겹치지 않게 미리 계획

write / read 분리

class axi4_os_write_seq extends axi4_os_base_seq;
    task body();
        for (int i = 0; i < OS_COUNT; i++) begin
            req = axi4_seq_item::type_id::create("req");
            start_item(req);
            if (!req.randomize() with { dir   == AXI_WRITE;
                                        burst == AXI_INCR;
                                        addr  == os_addr(i);
                                        len   == 3;
                                        size  == 3'd2;
                                        foreach (strb[j])
                                            strb[j] == {AXI_STRB_WIDTH{1'b1}}; })
                `uvm_error("RAND", "outstanding write randomize() failed")
            finish_item(req);
        end
    endtask
endclass
write와 read가 별도 시퀀스인 이유:
  test에서 사이에 drain을 넣어야 함
  → 읽기가 아직 안 끝난 쓰기를 추월하면 안 됨
  → (AXI 읽기/쓰기 채널 독립)

foreach (strb[j]) strb[j] == {AXI_STRB_WIDTH{1'b1}}
  = 모든 strobe 비트를 1로 (전체 바이트 쓰기)
  {N{1'b1}} = 1을 N번 반복 = 전부 1

이게 outstanding_test에서 봤던 "phase 1 쓰기 → drain → phase 2 읽기"의 시퀀스 쪽 구현이에요.


Part 4: cov_seq — 커버리지 채우기

// this sequence allows AXI_WRAP, so it can legitimately surface
// the DUT's WRAP addressing bug as scoreboard mismatches.

가장 복잡한 시퀀스. 세 단계로 커버리지를 채운다.

task body();
    foreach_burst_size();   // 모든 (burst × size) 조합
    burst_len_sweep();      // 모든 (burst × length class) 조합
    repeat (num) begin      // 넓은 랜덤 (length/region/strobe bin 채우기)
        req = axi4_seq_item::type_id::create("req");
        start_item(req);
        if (!req.randomize())
            `uvm_error("RAND", "coverage item randomize() failed")
        finish_item(req);
    end
endtask

왜 sweep이 필요한가

랜덤만으로는 특정 조합이 안 채워짐 (plateau)
  FIXED/WRAP은 가중치가 낮아서 (dist 8:1:1)
  → burst × length 교차 bin에 구멍이 남

해결: deterministic sweep으로 모든 조합을 명시적으로 방문
  → 랜덤이 못 가는 코너를 확정적으로 채움

이게 이력서의 "constrained-random (plateau 98.3%) + directed sweeps"의 실제 코드예요.

foreach_burst_size — 모든 (버스트 × 크기)

task foreach_burst_size();
    axi_burst_e blist[3] = '{AXI_FIXED, AXI_INCR, AXI_WRAP};
    bit [AXI_ADDR_WIDTH-1:0] a = 16'h2000;
    foreach (blist[b]) begin
        for (int s = 0; s <= 2; s++) begin
            automatic bit [7:0] l = (blist[b] == AXI_WRAP) ? 8'd3 : 8'd2;
            wr_then_rd(a, l, s[2:0], blist[b]);
            a += 16'h0100;
        end
    end
endtask
3가지 burst × 3가지 size = 9개 조합 전부 방문
  각각 wr_then_rd로 쓰고 읽음

WRAP일 때만 len=3 (4 beats, WRAP 합법)
나머지는 len=2

automatic = 각 반복마다 새 변수 (루프 변수 캡처 문제 방지)

burst_len_sweep — 모든 (버스트 × 길이)

task burst_len_sweep();
    bit [7:0] lens[4] = '{8'd0, 8'd2, 8'd6, 8'd12};  // 각 길이 클래스 대표

    foreach (lens[i]) begin
        wr_then_rd(a, lens[i], 3'd2, AXI_FIXED); a += 16'h0100;
        wr_then_rd(a, lens[i], 3'd2, AXI_INCR);  a += 16'h0100;
    end
    // WRAP은 2/4/8/16 beats만 합법
    wr_then_rd(a, 8'd1,  3'd2, AXI_WRAP); ...
    wr_then_rd(a, 8'd3,  3'd2, AXI_WRAP); ...
    wr_then_rd(a, 8'd7,  3'd2, AXI_WRAP); ...
    wr_then_rd(a, 8'd15, 3'd2, AXI_WRAP);
endtask
길이 클래스: single(0) / 2-4(2) / 5-8(6) / 9-16(12)
  각 클래스 대표값으로 FIXED, INCR 방문

WRAP은 합법 길이(1/3/7/15 = 2/4/8/16 beats)만 별도로

Part 5: directed_seq — 코너 케이스

task body();
    //          addr      len   size  burst        what it exercises
    wr_then_rd(16'h0000, 8'd0,  3'd2, AXI_INCR);  // single beat, full width
    wr_then_rd(16'h0100, 8'd15, 3'd2, AXI_INCR);  // max 16-beat burst
    wr_then_rd(16'h0FC0, 8'd15, 3'd2, AXI_INCR);  // ends exactly at 4KB edge
    wr_then_rd(16'h0200, 8'd7,  3'd0, AXI_INCR);  // narrow: 1 byte/beat
    wr_then_rd(16'h0300, 8'd3,  3'd1, AXI_INCR);  // narrow: 2 bytes/beat
    wr_then_rd(16'h0400, 8'd3,  3'd2, AXI_FIXED); // FIXED: same address
endtask
손으로 고른 위험한 코너들:

single beat     : len=0 → 1 beat (경계값)
max burst       : len=15 → 16 beats (최대)
4KB edge        : 0x0FC0 + 16beat×4B = 0x1000 딱 경계
narrow 1B/beat  : size=0 → lane_mask가 토글하는 케이스
narrow 2B/beat  : size=1
FIXED           : 같은 주소 반복

→ 랜덤이 잘 안 만드는 경계값을 확정적으로 테스트

이게 directed_test가 쓰는 시퀀스. 주석에 각 케이스가 뭘 테스트하는지 다 적혀있어요.


Part 6: wrap_seq — 버그 재현 (핵심)

// The reference model wraps correctly, the DUT does not,
// so this sequence is EXPECTED to raise scoreboard mismatches.

왜 WRAP write + INCR read인가 (가장 중요)

task wrap_write_incr_read(bit [AXI_ADDR_WIDTH-1:0] a,
                          bit [7:0]                l,
                          bit [2:0]                s);
    axi4_seq_item wr, rd;
    int unsigned nbytes = 1 << s;
    int unsigned total  = (l + 1) * nbytes;          // wrap window size
    bit [AXI_ADDR_WIDTH-1:0] base = a - (a % total); // window base

    // WRAP으로 쓰기 (window 중간에서 시작 → 실제로 wrap 발생)
    wr = axi4_seq_item::type_id::create("wr_wrap");
    start_item(wr);
    if (!wr.randomize() with { dir == AXI_WRITE; addr == a;
                               len == l; size == s; burst == AXI_WRAP;
                               foreach (strb[j])
                                   strb[j] == {AXI_STRB_WIDTH{1'b1}}; })
        `uvm_error("RAND", "wrap write randomize() failed")
    finish_item(wr);

    // INCR로 window 전체 읽기
    rd = axi4_seq_item::type_id::create("rd_incr");
    start_item(rd);
    if (!rd.randomize() with { dir == AXI_READ; addr == base;
                               len == l; size == s; burst == AXI_INCR; })
        `uvm_error("RAND", "incr read-back randomize() failed")
    finish_item(rd);
endtask

주석이 핵심을 설명한다:

// Why not a plain WRAP write + WRAP read?  Because that hides the
// bug: the DUT is consistently wrong on both sides, so it reads
// from the same (wrong) places it wrote to and the data matches.
// Only an INCR read exposes where the bytes actually landed.
왜 WRAP 쓰기 + WRAP 읽기가 아니라 WRAP 쓰기 + INCR 읽기?

WRAP write + WRAP read (대칭):
  DUT가 쓸 때도 틀리고 읽을 때도 똑같이 틀림
  → 틀린 위치에 쓰고, 그 틀린 위치에서 읽음
  → 데이터는 일치! → 버그 숨겨짐 ❌

WRAP write + INCR read (비대칭):
  DUT: WRAP 쓰기를 틀린(선형) 위치에 씀
  INCR 읽기: 올바른 순서로 window 전체를 읽음
  → "바이트가 실제로 어디 떨어졌나" 드러남
  → 예상과 불일치 → 버그 노출! ✅

이게 이력서의 "asymmetric WRAP-write / INCR-read cross-check that surfaced 24 mismatches"의 정확한 코드예요.

body — 실제 실행

task body();
    // len 1/3/7/15 -> 2/4/8/16 beats
    wrap_write_incr_read(16'h0A08, 8'd3, 3'd2);  // window 0x0A00..0x0A0F
    wrap_write_incr_read(16'h0B10, 8'd7, 3'd2);  // window 0x0B00..0x0B1F
endtask
0x0A08에서 WRAP 쓰기 시작 (window 0x0A00~0x0A0F 중간)
  → 0x0A08부터 쓰다가 0x0A0F 넘으면 0x0A00으로 되돌아와야 함 (정상)
  → DUT는 안 되돌아오고 계속 직진 (버그)

base(0x0A00)부터 INCR로 window 전체 읽기
  → 실제로 어디 썼는지 드러남 → 미스매치!

전체 시퀀스 지도

시퀀스WRAP 포함?특징
write/read/randN개 랜덤 (INCR/FIXED만)
wr_rd쓰고 읽기, 기본 자극
os_*고정 주소, 파이프라인용
cov커버리지 sweep (WRAP 버그 노출 가능)
directed손으로 고른 코너
wrap비대칭 cross-check (버그 재현)

한 줄 요약

Sequence = 어떤 트랜잭션을 어떤 순서로 만들지 정의하는 자극 프로그램

axi4_base_seq: 부모, wr_then_rd() 헬퍼 (쓰고 같은 영역 읽기)

핵심 설계:
  1. 일반 시퀀스는 WRAP 제외 → 정상 회귀 clean 유지
  2. WRAP은 wrap_seq로 격리 → 비대칭(WRAP write + INCR read)으로
     버그를 "일부러" 노출
  3. cov_seq는 deterministic sweep으로 랜덤 plateau의 구멍을 채움

start_item/finish_item = driver의 get_next_item/item_done과 짝
randomize() with {...} = seq_item 제약 위에 추가 조건

시리즈 마무리 — 전체 데이터 경로

이제 AXI4 UVM 환경의 모든 파일을 봤습니다.

tb_top          → 조립 & 시작
cfg/agent/env   → 계층 빌드
base_test       → 지휘 (get_seq로 시퀀스 선택)
sequence        → 자극 생성 (이 글!)  ← randomize()
    ↓
seq_item        → 트랜잭션 단위
    ↓
driver          → 핀 구동
    ↓ (인터페이스 = 신호 + clocking block)
DUT
    ↓
monitor         → 관찰 & 재조립
    ↓
scoreboard      → ref_model과 비교 → pass/fail

자극이 생성(sequence)되어 아이템(seq_item)으로 만들어지고, 드라이버가 핀을 흔들어 DUT에 전달하고, 모니터가 관찰해서 스코어보드가 정답지와 비교하는 — 검증의 전체 순환이 완성됩니다.

profile
Design Verification engineer

0개의 댓글