EDA Playground'da Dene

Lab 5: Interface + Coverage

Gün 5: Arayüzler, Assertion ve Coverage | Interface, Virtual Interface, Driver ve Coverage entegrasyonu

Bu lab, Gün 5 boyunca öğrendiğimiz tüm parçaları (interface, clocking block, virtual interface, transaction sınıfı, driver ve functional coverage) tek bir mini doğrulama ortamında birleştiriyor. Hedef, basit bir bellek DUT'unu mailbox tabanlı bir driver ile sürmek ve sürülen işlemlerin kapsamını ölçmek.

Lab'ın Mimarisi

Bu test ortamı, gerçek bir UVM ortamının sadeleştirilmiş bir taslağıdır. Akış şöyle ilerler:

  • Interface + clocking block (mem_if): DUT ile testbench arasındaki sinyalleri gruplar; cb clocking block'u senkronizasyonu sağlar. tb ve dut modport'ları iki tarafın bakış açısını ayırır.
  • Transaction (MemTransaction): Tek bir bellek işlemini (yazma/okuma, adres, veri) temsil eden, rastgele üretilebilen bir veri nesnesidir.
  • Driver (MemDriver): Bir mailbox üzerinden gelen transaction'ları alır ve virtual interface aracılığıyla DUT'a sürer.
  • Coverage (MemCoverage): Üretilen işlemlerin kapsamını covergroup ile ölçer.
  • Top (lab5_interface_coverage): Tüm bileşenleri bağlar, driver'ı arka planda çalıştırır ve işlemleri üretip mailbox'a koyar.

Producer–Consumer ve Mailbox

Bu labın can damarı mailbox ile kurulan üretici–tüketici (producer–consumer) modelidir. Üst modül işlemleri üretip mbx.put(txn) ile kuyruğa koyar; driver ise mbx.get(txn) ile bunları sırayla alıp sürer. mailbox, iki süreç arasında güvenli ve senkron veri aktarımı sağlar; tüketici hazır olana dek üreticiyi doğal biçimde dengeler. Bu ayrıştırma, stimulus üretimini sinyal seviyesinden bağımsız kılar — UVM'deki sequencer/driver ayrımının temelidir.

Mimari Şema

 lab5_interface_coverage (top)
 ├── clk ────────────────────────────────┐
 ├── mem_if mif(clk)  ◀── virtual ───┐   │
 │     ├─ clocking cb (tb tarafi)    │   │
 │     ├─ modport tb                 │   │
 │     └─ modport dut ──▶ memory_dut (DUT)
 ├── mailbox mbx ──▶ MemDriver drv (vif = mif.tb) ── cb ──▶ DUT pinleri
 ├── MemCoverage cov  ◀── cov.sample(txn)   (stimulus uretilirken)
 └── initial: reset → fork drv.run() → 50 × {randomize, sample, put} → wait → rapor

Adım Adım Görev Listesi

  1. Interface: mem_if içinde sinyaller, cb clocking block'u (output cs, we, addr, wdata; input rdata, ack) ve tb/dut modport'ları.
  2. DUT: Senkron, rst_n ile asenkron sıfırlanan 256 kelimelik bellek; cs aktifken yaz/oku ve ack üret.
  3. Transaction: rand we, addr, wdata; adres 0–63; solve we before wdata ile yön bağımlı veri kısıtı.
  4. Driver: virtual mem_if.tb vif + mailbox; run() içinde get → sür → ack bekle → rdata oku → cs düşür → bir idle çevrim.
  5. Coverage: with function sample(...) covergroup'u, cp_we, cp_addr, cp_data ve cx_we_addr.
  6. Top: Reset, join_none ile driver, 50 transaction, wait(drv.driven_count == 50), rapor.

Kontrol noktası: cp_we %100, cp_addr %100 ve cx_we_addr %100'e yakın çıkmalı (8 kombinasyon, 50 örnek). cp_data %100 olmalı: okuma işlemleri zero, yazmalar nonzero bin'ini doldurur.

Kaynak Kod

// =============================================================================
// GÜN 5 - Lab 5: Interface + Virtual Interface + Driver + Coverage (Kapsam)
// =============================================================================

// Arayüz (Interface) tanımı
interface mem_if(input logic clk);
  logic        rst_n;
  logic        cs;       // çip seçme (chip select)
  logic        we;       // yazma yetkisi (write enable)
  logic [7:0]  addr;
  logic [31:0] wdata;
  logic [31:0] rdata;
  logic        ack;

  clocking cb @(posedge clk);
    default input #1step output #0;
    output cs, we, addr, wdata;
    input  rdata, ack;
  endclocking

  modport tb  (clocking cb, output rst_n, input clk);
  modport dut (input clk, rst_n, cs, we, addr, wdata, output rdata, ack);
endinterface

// Basit Bellek Tasarımı (DUT)
module memory_dut(mem_if.dut mif);
  logic [31:0] mem [256];

  always @(posedge mif.clk or negedge mif.rst_n) begin
    if (!mif.rst_n) begin
      mif.rdata <= 0;
      mif.ack   <= 0;
    end else if (mif.cs) begin
      if (mif.we) begin
        mem[mif.addr] <= mif.wdata;
      end else begin
        mif.rdata <= mem[mif.addr];
      end
      mif.ack <= 1;
    end else begin
      mif.ack <= 0;
    end
  end
endmodule

// İşlem (Transaction) sınıfı
class MemTransaction;
  rand bit         we;
  rand bit [7:0]   addr;
  rand logic [31:0] wdata;
  logic [31:0]    rdata;
  int              id;
  static int       count = 0;

  constraint c_addr {
    addr inside {[0:63]};
  }

  //
  constraint c_data {
    solve we before wdata; // ÖNEMLİ: Önce we sinyalini %50 olasılıkla hesaplasın diye koyduk
    we -> wdata != 0;
    !we -> wdata == 0;
  }

  function new();
    id = count++;
  endfunction

  function void display(string prefix = "");
    $display("  %sTXN#%0d | %s | Addr=0x%02h | WData=0x%08h | RData=0x%08h",
             prefix, id, we ? "WR" : "RD", addr, wdata, rdata);
  endfunction
endclass

// Driver sınıfı (sanal arayüz - virtual interface kullanarak)
class MemDriver;
  virtual mem_if.tb vif;
  mailbox #(MemTransaction) mbx;
  int driven_count = 0;

  function new(virtual mem_if.tb vif, mailbox #(MemTransaction) mbx);
    this.vif = vif;
    this.mbx = mbx;
  endfunction

  task run();
    MemTransaction txn;
    forever begin
      mbx.get(txn);
      @(vif.cb);
      vif.cb.cs    <= 1;
      vif.cb.we    <= txn.we;
      vif.cb.addr  <= txn.addr;
      vif.cb.wdata <= txn.wdata;
      
      // Onay (ack) sinyalini senkronize şekilde bekle (simülatörün asılı kalmasını önler)
      do begin
        @(vif.cb);
      end while (vif.cb.ack == 0);

      if (!txn.we)
        txn.rdata = vif.cb.rdata;
        
      vif.cb.cs <= 0;
      
      // Tasarımın (DUT) CS=0 sinyalini görmesi ve ACK'yı düşürmesi için boş (idle) döngü
      @(vif.cb);
      
      driven_count++;
      txn.display("[Driver] ");
    end
  endtask

  task reset();
    vif.rst_n = 0;
    vif.cb.cs    <= 0;
    vif.cb.we    <= 0;
    vif.cb.addr  <= 0;
    vif.cb.wdata <= 0;
    repeat(5) @(vif.cb);
    vif.rst_n = 1;
    @(vif.cb);
    $display("  [Driver] Reset tamamlandı");
  endtask
endclass

// Kapsam (Coverage) sınıfı
class MemCoverage;
  covergroup mem_cg with function sample(bit we, bit [7:0] addr, logic [31:0] data);
    cp_we: coverpoint we {
      bins read  = {0};
      bins write = {1};
    }

    cp_addr: coverpoint addr {
      bins low   = {[0:15]};
      bins mid   = {[16:31]};
      bins high  = {[32:47]};
      bins upper = {[48:63]};
    }

    cp_data: coverpoint data {
      bins zero    = {0};
      bins nonzero = {[1:$]};   // 1'den en buyuk degere kadar TEK bin ($ = ust sinir)
      // NOT: 32-bit veri icin bins yazmazsaniz arac otomatik 64 kova uretir;
      // "nonzero = default" yazmak ise bu bin'i kapsam yuzdesine SAYMAZ.
    }

    cx_we_addr: cross cp_we, cp_addr;
  endgroup

  // Covergroup, içine gömüldüğünde kendi örnek tipi (instance type) gibi davranır
  function new();
    mem_cg = new();
  endfunction

  function void sample(MemTransaction txn);
    mem_cg.sample(txn.we, txn.addr, txn.wdata);
  endfunction

  function void report();
    $display("\n  --- Kapsam Raporu (Coverage Report) ---");
    $display("  Toplam      : %.1f%%", mem_cg.get_coverage());
    $display("  cp_we       : %.1f%%", mem_cg.cp_we.get_coverage());
    $display("  cp_addr     : %.1f%%", mem_cg.cp_addr.get_coverage());
    $display("  cp_data     : %.1f%%", mem_cg.cp_data.get_coverage());
    $display("  cx_we_addr : %.1f%%", mem_cg.cx_we_addr.get_coverage());
  endfunction
endclass

// Üst seviye test ortamı (Testbench)
module lab5_interface_coverage;
  logic clk = 0;
  always #5 clk = ~clk;

  mem_if mif(clk);
  memory_dut dut(mif);

  mailbox #(MemTransaction) mbx;
  MemDriver    drv;
  MemCoverage cov;

  initial begin
    // Örtük statik değişken hatalarını önlemek için döngü dışında tanımlanmıştır
    MemTransaction txn;

    $display("============================================================");
    $display("  LAB 5: Interface + Driver + Coverage");
    $display("============================================================\n");

    mbx = new();
    drv = new(mif, mbx);
    cov = new();

    // Reset dizisi
    drv.reset();

    // Driver'ı arka planda çalıştır
    fork
      drv.run();
    join_none

    // İşlemleri (transaction) oluştur ve kuyruğa (mailbox) koy
    repeat (50) begin
      txn = new(); // Nesneyi burada başlat
      assert(txn.randomize()) else $fatal(1, "Randomize başarısız!");
      cov.sample(txn);
      mbx.put(txn);
    end

    // Tüm işlemlerin tamamen sürülmesini bekle
    wait(drv.driven_count == 50);
    #100;

    // Raporlama
    $display("\n  --- İstatistikler ---");
    $display("  Toplam sürülen: %0d", drv.driven_count);
    cov.report();

    $display("\n============================================================");
    $display("  LAB 5 TAMAMLANDI");
    $display("============================================================");
    $finish;
  end
endmodule

Kodun Açıklaması

  • mem_if arayüzü: Bellek sinyallerini (rst_n, cs, we, addr, wdata, rdata, ack) gruplar. clocking cb bloğu default input #1step output #0 ile zamanlamayı yönetir. tb modport'u clocking block'u ve rst_n çıkışını içerir; dut modport'u sentezlenebilir tarafı tanımlar.
  • memory_dut modülü: posedge mif.clk or negedge mif.rst_n ile çalışır. Reset'te çıkışları sıfırlar; mif.cs aktifken mif.we ise mem[mif.addr]'a yazar, değilse mif.rdata'ya okur ve ack'i 1 yapar. cs düştüğünde ack'i geri 0'a çeker.
  • MemTransaction sınıfı: we, addr, wdata rastgele üretilir. c_addr adresi 0-63'e sınırlar. c_data kısıtında solve we before wdata ile önce we çözülür (yazma/okuma ~%50 dengesi için); we ise wdata != 0, değilse wdata == 0 zorlanır. static int count ile her nesneye benzersiz id atanır.
  • MemDriver sınıfı: virtual mem_if.tb vif ve mailbox #(MemTransaction) mbx tutar. run task'i forever döngüsünde mbx.get(txn) ile işlem alır, vif.cb üzerinden DUT'u sürer, do ... while (vif.cb.ack == 0) ile senkron olarak ack'i bekler, okuma ise txn.rdata'yı örnekler. cs'yi düşürüp bir idle çevrim bekler ve driven_count'u artırır. reset task'i rst_n'yi indirip 5 çevrim sonra geri kaldırır.
  • MemCoverage sınıfı: mem_cg covergroup'u cp_we (read/write), cp_addr (dört adres bölgesi) ve cp_data (zero/nonzero) coverpoint'lerini ve cx_we_addr çaprazını içerir. cp_data içindeki {[1:$]} aralığı "1'den tipin en büyük değerine kadar" demektir; $ burada üst sınırı temsil eder ve 32-bit için 32'hFFFF_FFFF yazmaktan kurtarır. sample(MemTransaction txn) sarmalayıcı fonksiyonu transaction alanlarını covergroup'a aktarır. report kapsam yüzdelerini basar.
  • lab5_interface_coverage (top): mem_if, DUT, mbx, drv ve cov oluşturulur. drv.reset() sonrası fork ... join_none ile driver arka planda başlatılır. repeat(50) döngüsünde her işlem randomize edilip cov.sample(txn) ile örneklenir ve mbx.put(txn) ile kuyruğa konur. wait(drv.driven_count == 50) tüm işlemlerin sürülmesini bekler, sonra rapor basılır.

Önemli Noktalar

  • solve ... before sıralamayı yönetir: solve we before wdata, çözücünün önce we'yi belirlemesini sağlayarak yazma/okuma dağılımını dengeler; c_data kısıtının yön bağımlılığı bu sayede tutarlı çalışır.
  • ack'i wait yerine senkron bekleyin: do begin @(vif.cb); end while (vif.cb.ack == 0) kullanımı, simülatörün asılı kalmasını ve yarış koşullarını engeller.
  • Geniş coverpoint'lerde bin'leri siz tanımlayın: 32-bit data için bins yazılmazsa araç otomatik 64 kova üretir ve anlamsız bir yüzde çıkar. nonzero = {[1:$]} tek ve anlamlı bir kova verir ve kapsam yüzdesine sayılır; nonzero = default yazımı ise çalışır ama default bin yüzdeye dahil edilmediği için cp_data yalnızca zero üzerinden hesaplanırdı.
  • Mailbox ile bileşenleri ayırın: Stimulus üretimi (top) ile sinyal sürüşü (driver) mailbox üzerinden ayrıştırılmıştır; bu, ölçeklenebilir ve yeniden kullanılabilir bir yapıdır.
  • Kapsamı sürüşten önce örnekleyin: Burada cov.sample(txn) işlem mailbox'a konmadan önce çağrılır; kapsam, gönderilen stimulus'a göre ölçülür. Gerçek doğrulamada genellikle monitör gözlemini örneklemek tercih edilir.
  • join_none ile arka plan süreci: Driver fork ... join_none ile başlatıldığından üst akış engellenmez; senkronizasyon driven_count üzerinden wait ile sağlanır. Bu, Lab 4'teki sabit #100 beklemesinden daha sağlamdır: transaction sayısı değişse de kod değişmez.
  • Okuma işlemlerinde rdata driver tarafından doldurulur: txn.rdata = vif.cb.rdata satırı, mailbox'tan gelen aynı nesneye yazar; handle paylaşımı sayesinde top'taki döngü bu nesneye hâlâ erişebilseydi okunan değeri görebilirdi. Gün 7'de bu bilgi scoreboard'a taşınacak.
  • solve we before wdata olmadan ne olurdu? Kısıt we -> wdata != 0; !we -> wdata == 0; çözücü için iki çözüm kümesi tanımlar: {we=1, wdata ∈ 1..2³²-1} (4 milyar çözüm) ve {we=0, wdata=0} (tek çözüm). Çözücü çözümleri eşit olasılıkla seçerse we=0 neredeyse hiç çıkmaz. solve ... before önce we'yi %50/%50 seçtirir, sonra wdata'yı ona göre çözer.

Ek Görevler (Zorluk Artan)

  1. Kolay: solve we before wdata; satırını silip 50 transaction içinde kaç okuma (RD) çıktığını sayın. Beklentinizle karşılaştırın.
  2. Orta: MemCoverage'a cp_rdata ekleyin ve sample()'ı driver sürdükten sonra çağırın (driver'a bir MemCoverage cov handle'ı verin). Stimulus'tan toplanan kapsamla DUT'tan okunan kapsamı karşılaştırın.
  3. Orta: mem_if içine bir assertion ekleyin: cs düştükten bir çevrim sonra ack da düşmeli ($fell(cs) |=> !ack). DUT'u bozup (örneğin ack <= 1 sabitleyip) assertion'ın yakaladığını gösterin.
  4. Zor: Bir MemMonitor sınıfı yazın (virtual mem_if.mon vif, tümü-input bir mon_cb ile — önceki dersteki kalıp) ve cs && ack anlarında MemTransaction üretip bir kuyruğa koysun. Simülasyon sonunda monitor kuyruğundaki işlem sayısının driven_count'a eşit olduğunu doğrulayın; ardından yazılan her adrese yapılan okumanın yazılan değeri döndürdüğünü kontrol eden basit bir scoreboard ekleyin.
Hızlı Kontrol: solve we before wdata dağılımı neden değiştirir?

Çözücü varsayılan olarak tüm çözüm kombinasyonlarını eşit olasılıkla seçer. we=1 için milyarlarca wdata çözümü, we=0 için tek çözüm varsa we=0 neredeyse hiç seçilmez. solve we before wdata, önce we'yi kendi aralığında (0/1, %50 olasılıkla) seçtirip wdata'yı ardından çözer; böylece okuma/yazma dengesi korunur.