EDA Playground'da Dene

Clocking Block

Gün 5: Arayüzler, Assertion ve Coverage | Zamanlama blokları, setup/hold yönetimi, APB protokolü

🌟🌟Okuma Önerisi: Taming Testbench Timing

Bu derste clocking block (zamanlama bloğu) yapısını ve test ortamı ile DUT arasındaki yarış koşullarını (race condition) nasıl ortadan kaldırdığını öğreneceğiz. Örnek olarak basit bir APB protokolünü clocking block üzerinden süreceğiz.

Clocking Block Neden Gerekli?

Bir test ortamı (testbench), DUT ile aynı saat kenarında sinyal sürerse iki kritik problem ortaya çıkar:

  • Yarış koşulu (race condition): TB sinyali tam saat kenarında değiştirir, DUT da aynı kenarda örnekler. Hangisinin önce çalışacağı belirsizdir; simülatöre göre sonuç değişebilir.
  • Setup/Hold belirsizliği: Gerçek donanımda sinyaller saat kenarından belirli bir süre önce kararlı olmalı (setup) ve kenardan sonra bir süre korunmalıdır (hold). Bunu modellemeden yapılan testler gerçekçi değildir.

clocking block, bir saat olayına (@(posedge clk)) göre sinyallerin ne zaman örnekleneceğini (input) ve ne zaman sürüleceğini (output) merkezi olarak tanımlar. Böylece testbench, DUT'tan otomatik olarak güvenli bir zaman aralığında ayrılır.

Input/Output Skew ve 1step

Koddaki default input #1step output #1 satırı zamanlamanın kalbidir:

  • input #1step: Girişler, saat kenarından bir simülasyon adımı önce (yani Preponed bölgesinde) örneklenir. Bu, DUT henüz aynı kenarda değer değiştirmeden, kararlı eski değeri okumayı garanti eder; böylece yarış koşulu yok olur.
  • output #1: Çıkışlar, saat kenarından 1 zaman birimi sonra sürülür. Bu, DUT'un girişleri kenarın hemen üzerinde değil, küçük bir gecikmeyle görmesini sağlar (hold benzeri davranış).

Test kodunda aif.cb.psel <= 1 gibi atamalar ve @(aif.cb) ile beklemeler, doğrudan ham sinyallere değil clocking block üzerinden yapıldığı için bu zamanlama kuralları otomatik uygulanır.

Kaynak Kod

// =============================================================================
// GUN 5 - Konu 2: Clocking Blocks (Zamanlama Bloklari)
// =============================================================================

interface apb_if(input logic clk);
  logic        psel;
  logic        penable;
  logic        pwrite;
  logic [7:0]  paddr;
  logic [31:0] pwdata;
  logic [31:0] prdata;
  logic        pready;

  // Clocking Block: Testbench zamanlama yonetimi
  // Sinyallerin ornekleme ve surme zamanlarini tanimlar
  clocking cb @(posedge clk);
    default input #1step output #1;   // Input output skew
    
    output psel;
    output penable;
    output pwrite;
    output paddr;
    output pwdata;
    input  prdata;
    input  pready;
  endclocking

  modport dut (
    input  clk, psel, penable, pwrite, paddr, pwdata,
    output prdata, pready
  );
endinterface

// Basit APB Slave (DUT rolunde)
module apb_slave(apb_if.dut apb);
  logic [31:0] reg_file [256];

  always @(posedge apb.clk) begin
    apb.pready <= 0;
    if (apb.psel && apb.penable) begin
      if (apb.pwrite)
        reg_file[apb.paddr] <= apb.pwdata;
      else
        apb.prdata <= reg_file[apb.paddr];
      apb.pready <= 1;
    end
  end
endmodule

module clocking_block;
  logic clk = 0;
  always #5 clk = ~clk;

  apb_if aif(clk);
  apb_slave dut(aif);

  // Clocking block ile surme
  task apb_write(input logic [7:0] addr, input logic [31:0] data);
    // Setup phase
    aif.cb.psel    <= 1;
    aif.cb.pwrite  <= 1;
    aif.cb.paddr   <= addr;
    aif.cb.pwdata  <= data;
    aif.cb.penable <= 0;
    @(aif.cb);

    // Access phase
    aif.cb.penable <= 1;
    @(aif.cb);
    wait(aif.cb.pready);

    // Idle
    aif.cb.psel    <= 0;
    aif.cb.penable <= 0;
    $display("  [%0t] APB Write: Addr=0x%02h, Data=0x%08h", $time, addr, data);
  endtask

  task apb_read(input logic [7:0] addr, output logic [31:0] data);
    aif.cb.psel    <= 1;
    aif.cb.pwrite  <= 0;
    aif.cb.paddr   <= addr;
    aif.cb.penable <= 0;
    @(aif.cb);

    aif.cb.penable <= 1;
    @(aif.cb);
    wait(aif.cb.pready);
    data = aif.cb.prdata;

    aif.cb.psel    <= 0;
    aif.cb.penable <= 0;
    $display("  [%0t] APB Read:  Addr=0x%02h, Data=0x%08h", $time, addr, data);
  endtask
  
  initial begin
    $dumpfile("dump.vcd"); 
    $dumpvars;
  end

  initial begin
    logic [31:0] rdata;
    $display("=== Clocking Block ===\n");

    // Baslangic degerleri
    aif.cb.psel    <= 0;
    aif.cb.penable <= 0;
    #20;

    // Yazma
    apb_write(8'h00, 32'hAAAA_BBBB);
    apb_write(8'h04, 32'hCCCC_DDDD);

    // Okuma
    apb_read(8'h00, rdata);
    apb_read(8'h04, rdata);

    #50;
    $display("\n=== Clocking Block Sonu ===");
    $finish;
  end
endmodule

Kodun Açıklaması

  • interface apb_if(input logic clk): APB sinyallerini (psel, penable, pwrite, paddr, pwdata, prdata, pready) gruplar.
  • clocking cb @(posedge clk): Tüm testbench zamanlamasını yöneten bloktur. default input #1step output #1 ile varsayılan örnekleme/sürme gecikmeleri belirlenir. TB'nin sürdüğü sinyaller (psel, penable, pwrite, paddr, pwdata) output; DUT'tan okunanlar (prdata, pready) input olarak tanımlanır.
  • modport dut: DUT'un bakış açısını verir; TB'nin sürdüğü sinyalleri input, ürettiklerini output alır. DUT clocking block'u kullanmaz, doğrudan ham sinyalleri kullanır.
  • apb_slave modülü: posedge apb.clk ile çalışır. Her saatte pready'yi varsayılan 0 yapar; apb.psel && apb.penable (APB access fazı) doğruysa pwrite'a göre reg_file'a yazar ya da oradan prdata'ya okur ve pready'yi 1 yapar.
  • apb_write task'i: APB'nin iki fazını uygular. Önce setup faz (psel=1, penable=0) kurulup @(aif.cb) ile bir saat beklenir; sonra access faz (penable=1) sürülür ve wait(aif.cb.pready) ile DUT onayı beklenir. Tüm atamalar aif.cb.* üzerinden, yani clocking block aracılığıyla yapılır.
  • apb_read task'i: Aynı setup/access akışını okuma için uygular ve data = aif.cb.prdata ile veriyi clocking block'tan örneklenmiş olarak alır.
  • clocking_block (top): clk üretir, apb_if aif(clk) ve apb_slave dut(aif) örneklenir. Başlangıç değerleri verildikten sonra iki yazma ve iki okuma işlemi yürütülür.

Önemli Noktalar

  • @(aif.cb) ile @(posedge clk) farkı: Clocking block olayını (@(aif.cb)) beklemek, ham saat kenarını beklemekten farklı olarak skew kurallarını devreye sokar ve yarış koşullarını engeller.
  • Clocking block sinyaline <= ile yazın: aif.cb.psel <= 1 gibi non-blocking atamalar, çıkış skew'ine (output #1) uygun şekilde değeri planlı zamanda sürer. Blocking atama (=) clocking block çıkışlarında uygun değildir.
  • 1step özel bir değerdir: #1step, zaman birimine bağlı olmayan en küçük adımdır ve Preponed bölgesinde örnekleme yaparak en güvenli giriş okumasını sağlar.
  • DUT clocking block kullanmaz: Zamanlama bloğu yalnızca testbench tarafının senkronizasyon aracıdır; DUT sentezlenebilir mantıkla ham sinyalleri kullanır.
  • Tek merkezden zamanlama: Tüm gecikme kararlarının clocking block'ta toplanması, dağınık #delay ifadelerine kıyasla bakımı kolaylaştırır ve hata riskini azaltır.
  • Clocking block girişleri okunur, çıkışları yazılır — tersi yasaktır. aif.cb.pready (input) okunabilir ama ona yazılamaz; aif.cb.psel (output) yazılabilir ama okunması standart dışıdır (araçlar hata ya da uyarı verir). Hem sürüp hem okumanız gereken bir sinyal varsa inout olarak tanımlayın ya da ham sinyali (aif.psel) okuyun.
  • Clocking olayı dışında yapılan sürüşler ertelenir: apb_write task'ının ilk satırındaki aif.cb.psel <= 1 çağrısı bir saat kenarında yapılmıyorsa, atama bir sonraki cb olayına (+ output skew) kadar bekletilir. Bu, tüm sürüşlerin otomatik olarak saat hizalı olmasını sağlar.
  • wait(aif.cb.pready) neden güvenli? Clocking block girişleri yalnızca saat kenarında (#1step öncesinde) örneklendiği için aif.cb.pready değeri çevrim ortasında değişmez; wait bu yüzden burada yarış yaratmaz. Ham aif.pready üzerinde aynı wait ise yarışa açıktır.

Skew Değerlerinin Anlamı

             ┌──────────────────┐
  clk  ──────┘                  └───────
        ▲    ▲      ▲
        │    │      └── output #1 : TB çıkışları kenardan 1 birim SONRA sürülür
        │    └───────── saat kenarı: DUT örnekler (Active bölge)
        └────────────── input #1step : TB girişleri kenardan hemen ÖNCE örneklenir (Preponed)
Skew Anlamı Ne zaman seçilir?
input #1step Kenardan hemen önceki kararlı değer (Preponed bölgesi) Varsayılan ve en güvenli giriş örneklemesi
input #0 Kenarda, Observed bölgesinde Nadiren; DUT'un aynı kenardaki NBA güncellemesini görmek istiyorsanız
output #0 Kenarda, Re-NBA bölgesinde sürülür Yaygın; DUT'un Active bölgedeki örneklemesinden sonra gelir, yarış yaratmaz
output #1 / #2 Kenardan N birim sonra sürülür Gerçekçi hold süresi istendiğinde; dalga formunda okunması daha kolay
input #2 Kenardan 2 birim önce örnekler Setup süresi modellemek için

Önemli ayrım: #1step zaman birimine bağlı değildir; simülatörün en küçük zaman çözünürlüğü kadardır. #1 ise timescale'e bağlıdır (1 ns olabilir, 1 ps olabilir).

Benzetme: Clocking block, bir orkestradaki şeftir. Müzisyenler (testbench süreçleri) "şu anda çal" demez; şefin işaretini (@(aif.cb)) bekler ve şefin kuralına göre işaretten biraz önce nefes alır (input skew), biraz sonra notaya basar (output skew). Kimse şefi beklemeden çalmaya kalkmaz, bu yüzden orkestra (TB ve DUT) birbirine karışmaz.

Sık Yapılan Hatalar

  • Clocking çıkışını okumak: if (aif.cb.psel) yazmak araca göre hata/uyarıdır. Sürdüğünüz değeri biliyorsunuz; gerekirse yerel bir değişkende tutun.
  • Blocking atama (=) ile clockvar sürmek: aif.cb.psel = 1; derleme hatasıdır; clocking çıkışları yalnızca <= (synchronous drive) ile sürülür.
  • @(posedge clk) ile @(aif.cb)'yi karıştırmak: Aynı anda uyanırlar gibi görünür ama @(aif.cb) sonrasında okunan cb girişleri örneklenmiş değerlerdir; ham kenar beklemede ise ham sinyal okursunuz ve yarışa düşersiniz.
  • DUT'u clocking block ile sürmeye çalışmak: Clocking block testbench tarafı içindir; DUT ham sinyalleri kullanır (modport dut).
  • #1step'i #1 sanmak: timescale 1ns/1ps ile #1 = 1 ns, #1step = 1 ps'dir. Preponed örnekleme için daima #1step.

Kendinizi Deneyin

  1. default input #1step output #1 satırını output #0 yapın ve dalga formunda psel'in saat kenarına göre nerede değiştiğini karşılaştırın.
  2. apb_write task'ında aif.cb.psel <= 1; yerine aif.psel <= 1; (ham sinyal) yazın. Çalıştı mı? $dumpvars ile hangi anda sürüldüğünü inceleyin; clocking block sürüşünden farkını açıklayın.
  3. if (aif.cb.psel) ifadesini bir yere ekleyin; derleyicinin verdiği mesajı not alın. Sonra psel'i inout yapıp tekrar deneyin.
  4. apb_slave'e "yazma işlemi 2 çevrim sürer" davranışı ekleyin (pready ikinci çevrimde 1 olsun). apb_write'taki wait(aif.cb.pready) bunu değişiklik gerektirmeden kaldırabildi mi?
Hızlı Kontrol: input #1step ne anlama gelir ve neden yarış koşulunu önler?

Girişlerin saat kenarından bir simülasyon adımı önce, yani Preponed bölgesinde örnekleneceği anlamına gelir. Bu anda DUT henüz o kenardaki güncellemelerini yapmamıştır; testbench her zaman önceki çevrimin kararlı değerini okur. DUT'un "aynı kenarda değişen" değerini mi yoksa eskisini mi okuduğunuz belirsizliği böylece kalkar.