EDA Playground'da Dene

Interface ve Modport

Gün 5: Arayüzler, Assertion ve Coverage | Sinyal gruplama, erişim yönü tanımlama, master/slave/monitor

Bu derste SystemVerilog interface yapısını ve modport mekanizmasını öğreneceğiz. Bir veri yolu (bus) protokolünü tek bir arayüz içinde toplayıp, bu arayüze bağlanan modüllerin (master, slave, monitor) sinyallere hangi yönde eriştiğini nasıl tanımlayacağımızı göreceğiz.

Interface Neden Var?

Klasik Verilog'da bir modülün portları tek tek listelenir; aynı sinyal grubu birçok modülde tekrar tekrar yazılır. Tasarıma yeni bir sinyal eklendiğinde, bu sinyali taşıyan her modül başlığını ve her bağlantı satırını elle güncellemek gerekir. Bu hem yorucu hem de hataya açıktır.

interface, birbirine ait sinyalleri tek bir mantıksal birim altında gruplar:

  • Bakımı kolaylaştırır: Protokole yeni bir sinyal eklemek için sadece arayüzü güncellersiniz; bağlantılar olduğu gibi kalır.
  • Bağlantıyı sadeleştirir: Modülleri tek bir arayüz örneği (bif gibi) ile bağlarsınız, onlarca sinyali tek tek bağlamazsınız.
  • Yeniden kullanılabilir: Aynı arayüz tasarımda (RTL) ve doğrulama ortamında (testbench) ortak dil olur.

Modport: Erişim Yönünü Tanımlama

Bir arayüze birden fazla bileşen bağlanır ve her biri aynı sinyali farklı yönde kullanır. Örneğin valid sinyali master için bir çıkış, slave için bir giriştir. İşte modport (module port) tam olarak bu "bakış açısını" tanımlar:

  • master işlemi başlatır: valid, rw, addr, wdata üretir; ready, rdata, resp okur.
  • slave işleme yanıt verir: master'ın çıkışlarını dinler, ready/rdata/resp üretir.
  • monitor hiçbir şey sürmez; tüm sinyalleri yalnızca dinler (hepsi input). Bu, gözlemcinin tasarımı yanlışlıkla etkilemesini önler.

Modport sayesinde derleyici, bir modülün yanlış yönde sinyal sürmesini yakalayabilir ve kod okunabilirliği artar.

Kaynak Kod

// =============================================================================
// GUN 5 - Konu 1: Interface Kavrami ve Modports
// =============================================================================

// Interface tanimi: Sinyalleri gruplar
interface bus_if(input logic clk);
  logic        rst_n;
  logic        valid;
  logic        ready;
  logic        rw;        // 0=read, 1=write
  logic [7:0]  addr;
  logic [31:0] wdata;
  logic [31:0] rdata;
  logic        resp;      // 0=OK, 1=ERROR

  // Modport: Her bilesenin sinyallere erisim yonunu tanimlar
  modport master (
    input  clk, rst_n, ready, rdata, resp,
    output valid, rw, addr, wdata
  );

  modport slave (
    input  clk, rst_n, valid, rw, addr, wdata,
    output ready, rdata, resp
  );

  modport monitor (
    input clk, rst_n, valid, ready, rw, addr, wdata, rdata, resp
  );
endinterface

// Master module
module bus_master(bus_if.master bus);
  int txn_count = 0;

  task write(input logic [7:0] address, input logic [31:0] data);
    @(posedge bus.clk);
    bus.valid <= 1;
    bus.rw    <= 1;
    bus.addr  <= address;
    bus.wdata <= data;

    // Ready sinyalini clock ile senkron bir şekilde bekliyoruz
	// wait ile beklersek ne olurdu??
    do begin
      @(posedge bus.clk);
    end while (!bus.ready);

    bus.valid <= 0;
    txn_count++;
    $display("  [%0t][Master] WRITE: Addr=0x%02h, Data=0x%08h", $time, address, data);
  endtask

  task read(input logic [7:0] address, output logic [31:0] data);
    @(posedge bus.clk);
    bus.valid <= 1;
    bus.rw    <= 0;
    bus.addr  <= address;

    // Ready sinyalini clock ile senkron bir şekilde bekliyoruz
	// wait ile beklersek ne olurdu??
    do begin
      @(posedge bus.clk);
    end while (!bus.ready);

    data = bus.rdata;
    bus.valid <= 0;
    txn_count++;
    $display("  [%0t][Master] READ:  Addr=0x%02h, Data=0x%08h", $time, address, data);
  endtask
endmodule

// Slave module
module bus_slave(bus_if.slave bus);
  logic [31:0] memory [256];

  always @(posedge bus.clk or negedge bus.rst_n) begin
    if (!bus.rst_n) begin
      bus.ready <= 0;
      bus.rdata <= 0;
      bus.resp  <= 0;
    end else begin
      // Default state for ready
      bus.ready <= 0;

      // Process transaction only if valid is high and we are not already ready
      if (bus.valid && !bus.ready) begin
        if (bus.rw) begin  // Write operation
          memory[bus.addr] <= bus.wdata;
          bus.resp <= 0;
        end else begin     // Read operation
          bus.rdata <= memory[bus.addr];
          bus.resp  <= 0;
        end
        
        // Assert ready for one clock cycle to complete the handshake
        bus.ready <= 1;
      end
    end
  end
endmodule

// Monitor modulu
module bus_monitor(bus_if.monitor bus);
  int total_reads = 0, total_writes = 0;

  always @(posedge bus.clk) begin
    if (bus.valid && bus.ready) begin
      if (bus.rw) begin
        total_writes++;
        $display("  [%0t][Monitor] WRITE gozlemlendi: Addr=0x%02h", $time, bus.addr);
      end else begin
        total_reads++;
        $display("  [%0t][Monitor] READ gozlemlendi:  Addr=0x%02h", $time, bus.addr);
      end
    end
  end
endmodule

// Top-level
module interface_modport;
  logic clk = 0;
  always #5 clk = ~clk;

  // Interface ornekleme
  bus_if bif(clk);

  // Modul baglantilari
  bus_master  mst(bif);
  bus_slave   slv(bif);
  bus_monitor mon(bif);

  logic [31:0] read_data;

  initial begin
    $dumpfile("dump.vcd"); $dumpvars;
    $display("=== Interface ve Modport ===\n");

    // Reset
    bif.rst_n = 0;
    #20;
    bif.rst_n = 1;
    #10;

    // Yazma islemleri
    mst.write(8'h10, 32'hDEAD_BEEF);
    mst.write(8'h20, 32'hCAFE_BABE);
    mst.write(8'h30, 32'h1234_5678);

    #20;

    // Okuma islemleri
    mst.read(8'h10, read_data);
    mst.read(8'h20, read_data);

    #50;
    $display("\n  --- Istatistikler ---");
    $display("  Master islem sayisi: %0d", mst.txn_count);
    $display("  Monitor - Read: %0d, Write: %0d", mon.total_reads, mon.total_writes);

    $display("\n=== Interface ve Modport Sonu ===");
    
    $finish;
  end
endmodule

Kodun Açıklaması

  • interface bus_if(input logic clk): Tüm veri yolu sinyallerini (rst_n, valid, ready, rw, addr, wdata, rdata, resp) tek çatı altında toplar. Saat (clk) arayüze port olarak verilir, böylece tüm bağlı modüller aynı saati paylaşır.
  • modport master, modport slave, modport monitor: Aynı sinyal kümesini üç farklı bakış açısıyla sunar. Dikkat edin: valid master'da output, slave'de input olarak görünür; monitor ise her şeyi input alır.
  • bus_master modülü: Portunda bus_if.master bus yazarak yalnızca master görünümünü alır. write ve read task'leri el sıkışmayı yönetir: bus.valid kaldırılır, ardından do ... while (!bus.ready) döngüsüyle slave'in ready sinyali saat kenarına senkron biçimde beklenir. txn_count ile yapılan işlem sayısı tutulur.
  • bus_slave modülü: posedge bus.clk or negedge bus.rst_n ile çalışır. Reset sırasında çıkışları sıfırlar; aksi halde her saatte ready'yi varsayılan olarak 0 yapar, bus.valid && !bus.ready koşulunda işlemi gerçekleştirir. bus.rw 1 ise memory[bus.addr]'a yazar, 0 ise oradan bus.rdata'ya okur ve el sıkışmayı tamamlamak için ready'yi bir saat süresince 1 yapar.
  • bus_monitor modülü: Her saat kenarında bus.valid && bus.ready (başarılı el sıkışma anı) gerçekleştiğinde işlemi total_reads/total_writes sayaçlarıyla kayıt altına alır; hiçbir sinyali sürmez.
  • interface_modport (top): clk üretir, bus_if bif(clk) ile arayüzü örnekler ve mst, slv, mon modüllerini aynı bif örneğiyle bağlar. Reset uygulanır, üç yazma ve iki okuma işlemi yürütülür, sonunda istatistikler basılır.

Önemli Noktalar

  • Modport yönü doğrulukları: Bir sinyali yanlış modportta output ilan etmek derleme hatası ya da sürüş çakışması yaratır. Master/slave'in karşıt yönleri tutarlı olmalıdır.
  • El sıkışmayı wait yerine saat kenarıyla bekleyin: Kodda do begin @(posedge bus.clk); end while (!bus.ready) kullanılması bilinçli bir tercihtir. wait(bus.ready) ile beklemek, ready aynı delta zaman diliminde dalgalanırsa yarış (race) ve yanlış senkronizasyona yol açabilir.
  • Monitor pasif kalmalı: Gözlemci modport'unun tüm sinyalleri input olmalı; aksi halde monitör DUT davranışını istemeden değiştirebilir.
  • Tek arayüz, çok bağlantı: Tasarıma sinyal eklemek istediğinizde yalnızca bus_if'i güncellemeniz yeterlidir; modül bağlantılarını değiştirmeniz gerekmez. İşin gücü buradadır.
  • Non-blocking atama kullanımı: Sıralı mantıkta (always @(posedge clk)) <= kullanılması, simülasyon yarışlarını engeller ve gerçek donanım davranışını yansıtır.
  • Interface portları ve parametreler: bus_if(input logic clk) gibi interface'ler port alabilir; ayrıca interface bus_if #(parameter int DW = 32) ile parametrelendirilebilir. Aynı interface'i 32-bit ve 64-bit veri yolları için yeniden kullanmanın yolu budur.
  • Interface içinde kod: Bir interface yalnızca sinyal listesi değildir; içine task/function (örneğin task do_write(...)), always blokları ve assertion'lar yazılabilir. Protokol kurallarını interface'e gömmek, o interface'i kullanan her testbench'in kuralları otomatik denetlemesini sağlar (Gün 5'in SVA dersinde göreceksiniz).

El Sıkışmanın Çevrim Çevrim Akışı

mst.write() çağrısının saat kenarlarında nasıl ilerlediğini görmek, "wait ile beklersek ne olurdu?" sorusunun cevabını verir:

Kenar Master Slave (always @(posedge)) Monitor
E0 valid<=1, addr, wdata (NBA → E0'dan sonra görünür) valid henüz 0 görür —
E1 do...while: ready 0 → bekle valid=1 && !ready → memory[addr]<=wdata, ready<=1 —
E2 ready 1 → döngüden çık, valid<=0, txn_count++ ready 1 olduğu için ready<=0 valid && ready → WRITE sayıldı
E3 Sonraki işlem valid 0 —

Her işlem 3 saat kenarı sürer ve ready tam bir çevrim yüksek kalır. Master ready'yi wait(bus.ready) ile bekleseydi, E1'deki NBA güncellemesinin hemen ardından (aynı zaman adımında) uyanır, valid<=0 yapar ve txn_count artırırdı; ama Monitor E2'de valid'i hâlâ 1 görür (NBA) — bu kez sayım tesadüfen tutar. Asıl sorun, wait ile uyanma anının delta döngüsüne bağlı olmasıdır: slave'in ready<=1 ve master'ın valid<=0 atamaları arasındaki sıra simülatöre göre değişebilir ve bazı araçlarda okuma işleminde rdata eski değer olarak alınır. Saat kenarına senkron bekleme bu belirsizliği tamamen kaldırır.

Benzetme: Interface, bir binadaki standart elektrik prizidir. Her cihaz (master, slave, monitor) aynı prize takılır ama hangi pinin faz, hangisinin nötr olduğu cihazın bakış açısına (modport) göre tanımlıdır. Sinyal eklemek "binaya yeni bir kablo çekmek" değil, "priz standardını güncellemek"tir; cihazların fişi aynı kalır.

Sık Yapılan Hatalar

  • Modport'ta yönü yanlış vermek: monitor modport'una output ready yazarsanız monitor kazara ready'yi sürebilir ve DUT davranışını değiştirir. Monitor'ü her zaman tümü input tutun.
  • Modport'suz bağlamak: module bus_master(bus_if bus) (modport yok) derlenir ama derleyici yön denetimi yapmaz; hatalar simülasyonda "çoklu sürücü" (multiple driver) olarak ortaya çıkar.
  • clk'yi interface içinde üretmek: Saati interface'e port olarak vermek (bus_if bif(clk)) tüm bileşenlerin aynı saati paylaşmasını garanti eder; interface içinde always #5 clk = ~clk yazmak, birden fazla interface örneğinde senkron olmayan saatler yaratır.
  • always @(posedge clk) içinde blocking (=) atama: Slave'in ready = 1 yazması master ile yarışa yol açar. Sıralı mantıkta <=.
  • Interface sinyaline iki yerden yazmak: Master valid sürerken test de bif.valid = 0 yazarsa son yazan kazanır; sinyal sahipliğini tek bileşende tutun.

Kendinizi Deneyin

  1. Master'daki do...while döngüsünü wait(bus.ready); ile değiştirin. read_data değerleri hâlâ doğru mu? $dumpvars ile dalga formunda ready/valid örtüşmesini inceleyin.
  2. bus_if'e logic [7:0] burst_len; sinyali ekleyin; yalnızca interface'i ve ilgili modport'ları güncelleyerek mst.write çağrısında burst uzunluğunu da sürün. Top modülde hiçbir bağlantı satırını değiştirmeniz gerekmediğini doğrulayın.
  3. bus_slave'e "adres 8'hF0'dan büyükse resp <= 1" kuralı ekleyin ve monitor'ün hata yanıtlarını ayrı saymasını sağlayın.
  4. interface bus_if #(parameter int DW = 32) şeklinde parametrelendirip wdata/rdata genişliğini DW yapın; top'ta bus_if #(.DW(64)) bif(clk); ile 64-bit örnekleyin.
Hızlı Kontrol: Bir monitor modport'unda tüm sinyaller neden input olmalıdır?

Monitor pasif bir gözlemcidir; sinyal sürmesi DUT davranışını değiştirir ve "gözlediği şeyi bozar". Tüm sinyalleri input yapmak, derleyicinin yanlışlıkla yazılan bir bus.ready <= 1 satırını derleme hatası olarak yakalamasını sağlar.