EDA Playground'da Dene

UVM Eğitimi 1. : Temeller ve Testbench Yapısı

Amaç

Bu dersin amacı, okuyanların SystemVerilog kullanarak temel bir UVM sınıf hiyerarşisi oluşturmasını, UVM raporlama mekanizmasını kullanmasını ve UVM fazlarının (phasing) nasıl çalıştığını uygulamalı olarak görmesini sağlamaktır.

Ön Koşullar

  • Temel SystemVerilog Nesne Yönelimli Programlama (OOP) bilgisi.
  • UVM kütüphanesinin derlenebildiği bir simülatör (Örn: Questa, Xcelium, VCS).

Lab 1.1: UVM Raporlama ve Fazların Gözlemlenmesi

Bu adımda basit bir UVM bileşeni (component) oluşturup, temel fazları (build_phase, connect_phase, run_phase) ezeceğiz (override) ve her bir fazın içine UVM raporlama makroları ekleyeceğiz.

Görev: my_agent adında uvm_agent sınıfından türetilen bir bileşen oluşturun.

package my_uvm_pkg;
// Import UVM package and macros
import uvm_pkg::*;
//`include "uvm_macros.svh"

// Define the agent class inheriting from uvm\_agent  
class my_agent extends uvm_agent;  
    
  // Register component to UVM factory  
  `uvm_component_utils(my_agent)  
    
  // Constructor  
  function new(string name = "my_agent", uvm_component parent = null);  
    super.new(name, parent);  
  endfunction  
    
  // Build phase: Executed top-down  
  virtual function void build_phase(uvm_phase phase);  
    super.build_phase(phase);  
    `uvm_info("AGENT_BUILD", "Executing build_phase for my_agent...", UVM_LOW)  
  endfunction  
    
  // Connect phase: Executed bottom-up  
  virtual function void connect_phase(uvm_phase phase);  
    super.connect_phase(phase);  
    `uvm_info("AGENT_CONN", "Executing connect_phase for my_agent...", UVM_LOW)  
  endfunction  
    
  // Run phase: Time-consuming phase, executed as a parallel task  
  virtual task run_phase(uvm_phase phase);  
    super.run_phase(phase);  
    `uvm_info("AGENT_RUN", "Starting run_phase for my_agent...", UVM_LOW)  
      
    // Simulate some delay  
    #10;  
      
    `uvm_info("AGENT_RUN", "Finished run_phase delay for my_agent.", UVM_HIGH)  
  endtask  
    
endclass

Kodun Açıklaması

Bu blok, ileride hiyerarşinin en alt yapı taşı olacak my_agent bileşenini tanımlar:

  • package my_uvm_pkg; ile tüm sınıflarımızı tek bir isim alanı (namespace) içine topluyoruz. import uvm_pkg::*; satırı ise UVM kütüphanesindeki sınıf ve makroların bu pakette görünür olmasını sağlar.
  • class my_agent extends uvm_agent; ifadesi, kendi sınıfımızı UVM'in hazır uvm_agent temel sınıfından türetir; böylece fazlama (phasing) ve raporlama altyapısını hazır olarak devralırız.
  • `uvm_component_utils(my_agent) makrosu sınıfı **UVM Factory**'ye kaydeder. Bu kayıt, ileride type_id::create ile nesne üretebilmenin ön koşuludur.
  • function new(string name, uvm_component parent) yapıcı metodu (constructor) bileşeni adı ve hiyerarşideki ebeveyni (parent) ile oluşturur; super.new(name, parent) çağrısı bu bilgiyi üst sınıfa iletir.
  • build_phase bir function'dır ve yukarıdan aşağıya (top-down) çalışır; içindeki super.build_phase(phase) çağrısı temel sınıfın kurulum işlemlerinin önce çalışmasını garanti eder. `uvm_info("AGENT_BUILD", ..., UVM_LOW) makrosu ise bir kimlik (ID), bir mesaj ve UVM_LOW detay seviyesiyle log basar.
  • connect_phase da bir function'dır ancak aşağıdan yukarıya (bottom-up) çalışır; burada bileşenler arası bağlantılar yapılır.
  • run_phase bir task olarak tanımlanmıştır çünkü içinde #10; gibi simülasyon zamanı harcayan ifadeler bulunur. Son `uvm_info mesajı UVM_HIGH seviyesinde basıldığı için varsayılan ayarlarda ekranda görünmez.

Lab 1.2: UVM Hiyerarşisinin (Testbench Ağacı) Kurulması

Agent bileşenimizi oluşturduk. Şimdi bu Agent'ı bir Environment (uvm_env) içine, Environment'ı da bir Test (uvm_test) içine yerleştireceğiz. UVM'de alt bileşenler her zaman bir üst bileşenin build_phase metodu içerisinde yaratılır.

Görev: my_env ve base_test sınıflarını oluşturun ve hiyerarşiyi bağlayın.

// Define the environment class  
class my_env extends uvm_env;  
  `uvm_component_utils(my_env)  
    
  // Handle for our agent  
  my_agent agent_inst;  
    
  function new(string name = "my_env", uvm_component parent = null);  
    super.new(name, parent);  
  endfunction  
    
  // Instantiate the agent in the build_phase using UVM factory  
  virtual function void build_phase(uvm_phase phase);  
    super.build_phase(phase);  
    `uvm_info("ENV_BUILD", "Building my_env...", UVM_LOW)  
      
    // Create the agent using the factory create method  
    agent_inst = my_agent::type_id::create("agent_inst", this);  
  endfunction  
endclass

// Define the base test class  
class base_test extends uvm_test;  
  `uvm_component_utils(base_test)  
    
  // Handle for our environment  
  my_env env_inst;  
    
  function new(string name = "base_test", uvm_component parent = null);  
    super.new(name, parent);  
  endfunction  
    
  // Instantiate the environment  
  virtual function void build_phase(uvm_phase phase);  
    super.build_phase(phase);  
    `uvm_info("TEST_BUILD", "Building base_test...", UVM_LOW)  
      
    // Create the environment  
    env_inst = my_env::type_id::create("env_inst", this);  
  endfunction  
    
  // Print the UVM topology in the end_of_elaboration phase  
  virtual function void end_of_elaboration_phase(uvm_phase phase);  
    super.end_of_elaboration_phase(phase);  
    uvm_top.print_topology();  
  endfunction  
    
  // Raise and drop objections in the run_phase to control simulation time  
  virtual task run_phase(uvm_phase phase);  
    // Raise objection to prevent simulation from ending immediately  
    phase.raise_objection(this);  
      
    `uvm_info("TEST_RUN", "Test is running...", UVM_LOW)  
    #50; // Wait for some simulation time  
    `uvm_info("TEST_RUN", "Test finished.", UVM_LOW)  
      
    // Drop objection to allow simulation to end  
    phase.drop_objection(this);  
  endtask  
endclass

Kodun Açıklaması

Bu blok, testbench ağacının orta ve üst katmanlarını kurar:

  • class my_env extends uvm_env; ile uvm_env temel sınıfından bir ortam (environment) türetilir ve `uvm_component_utils(my_env) ile Factory'ye kaydedilir.
  • my_agent agent_inst; satırı, ortamın içinde yaşayacak agent için bir tutamaç (handle) tanımlar; bu yalnızca bir göstergedir, nesne henüz yaratılmamıştır.
  • Nesne, my_env'in build_phase metodu içinde agent_inst = my_agent::type_id::create("agent_inst", this); ile yaratılır. type_id::create doğrudan new yerine kullanılır çünkü Factory üzerinden esnek (override edilebilir) nesne üretimi sağlar; this ifadesi yeni bileşenin ebeveynini belirtir.
  • class base_test extends uvm_test; ile en üstteki test sınıfı uvm_test'ten türetilir ve my_env env_inst; tutamacı tanımlanır. Test'in build_phase metodu, aynı şekilde env_inst = my_env::type_id::create("env_inst", this); ile ortamı yaratır. Bu zincir sayesinde Test → Env → Agent hiyerarşisi yukarıdan aşağıya kurulur.
  • end_of_elaboration_phase metodunun içindeki uvm_top.print_topology(); çağrısı, tüm bileşenler kurulduktan sonra testbench ağacını ekrana çizer. Bu faz, build ve connect tamamlandıktan sonra çalıştığı için tüm hiyerarşi hazırdır.
  • Test'in run_phase görevinde phase.raise_objection(this); ile simülasyonun erkenden bitmesi engellenir; #50; ile bir süre beklenir ve phase.drop_objection(this); ile itiraz geri çekilerek simülasyonun sonlanmasına izin verilir. raise/drop çiftinin dengeli olması kritiktir.

Lab 1.3: Top Modül ve run_test() Çağrısı

UVM sınıfları dinamiktir, donanım (DUT) ve arayüzler (Interfaces) ise statiktir. Simülasyonu başlatmak için standart bir SystemVerilog modülüne ve UVM'in beyni olan run_test() fonksiyonunu çağırmaya ihtiyacımız var.

Görev: tb_top modülünü yazın ve simülasyonu tetikleyin.

// Top level static module  
module tb_top;

  // Import UVM package  
  import uvm_pkg::*;
  import my_uvm_pkg::*;

  // Initial block to start the UVM phasing  
  initial begin  
    // Start the test named "base_test"  
    run_test("base_test");  
  end

endmodule

Kodun Açıklaması

Bu blok, dinamik UVM dünyası ile statik donanım dünyasını birleştiren köprüdür:

  • module tb_top; standart bir SystemVerilog modülüdür. UVM sınıfları çalışma zamanında (runtime) yaratılan dinamik nesnelerken, modüller simülasyonun başında var olan statik yapılardır; bu yüzden simülasyonu başlatacak giriş noktası bir modül olmak zorundadır.
  • import uvm_pkg::*; UVM çekirdeğini, import my_uvm_pkg::*; ise kendi yazdığımız my_agent, my_env ve base_test sınıflarını modülün içine görünür kılar.
  • initial begin ... end bloğu simülasyonun en başında bir kez çalışır.
  • run_test("base_test"); çağrısı UVM'in beynidir: argüman olarak verilen sınıf adını Factory'de arar, o sınıftan bir nesne yaratır ve tüm fazlama (build, connect, run vb.) mekanizmasını otomatik olarak başlatır. Test adını burada string olarak vermek, simülatöre +UVM_TESTNAME=... argümanıyla farklı testleri kod değiştirmeden çalıştırma esnekliği de tanır.

Önemli Noktalar

  • Factory kaydı şarttır: Her bileşeni `uvm_component_utils ile Factory'ye kaydedin. Bu olmadan type_id::create çalışmaz ve override mekanizmasından faydalanamazsınız.
  • Her zaman create, asla new değil: Bileşen nesnelerini type_id::create ile yaratın. new ile sabitlenen nesneler test esnekliğini ve yeniden kullanılabilirliği yok eder.
  • super çağrılarını silmeyin: build_phase, connect_phase gibi metotların başında super.<faz>(phase) çağrısını koruyun; aksi halde temel sınıfın arka plandaki kurulum ve konfigürasyon işlemleri atlanır.
  • Faz tipine dikkat: Zaman harcamayan kurulum işleri function fazlarında (build/connect), zaman akışı gerektiren senaryolar ise task fazlarında (run) yapılmalıdır.
  • Objection dengesi: run_phase içinde her raise_objection çağrısının karşılığında bir drop_objection olduğundan emin olun; aksi takdirde simülasyon ya hiç başlamaz ya da hiç bitmez.
  • Verbosity'yi bilinçli kullanın: Kritik mesajları UVM_LOW, detaylı bilgileri UVM_HIGH/UVM_DEBUG ile basın; böylece log kalabalığını +UVM_VERBOSITY argümanıyla istediğiniz anda yönetebilirsiniz.

Çalıştırma ve Beklenen Çıktılar

Bu kodları tek bir testbench.sv dosyası içinde toplayıp simülatörünüzde çalıştırdığınızda, aşağıdaki gibi bir çıktı görmelisiniz:

  1. Önce base_test, ardından my_env ve en son my_agent için build_phase logları (Yukarıdan Aşağıya - Top-Down).
  2. uvm_top.print_topology() sayesinde ekrana çizilmiş testbench ağacı.
  3. connect_phase logları (Aşağıdan Yukarıya - Bottom-Up).
  4. Zamanın akmaya başladığı run_phase logları.
  5. UVM_HIGH verbosity seviyesi ile yazılan logun (Finished run_phase delay...) varsayılan ayarlarda ekranda görünmediğini fark edeceksiniz. Bunu görmek için simülatöre +UVM_VERBOSITY=UVM_HIGH argümanı vermeniz gerekir.

UVM Eğitimi 1. Gün: Öğrenci Soru ve Cevap Rehberi

Soru 1: Sınıfların başında kullandığımız `uvm_component_utils() makrosu tam olarak ne işe yarıyor? Yazmazsak ne olur?

Cevap: Bu makro, oluşturduğumuz sınıfı UVM Factory adını verdiğimiz özel bir kayıt defterine kaydeder. Eğer bu makroyu yazmayı unutursanız, UVM Factory bu sınıfın varlığından haberdar olmaz. Bu durumda ileride nesneyi type_id::create() ile yaratmaya çalıştığınızda hata alırsınız. Ayrıca bu makro sayesinde ileride kodun hiçbir yerini değiştirmeden, bu sınıf yerine başka bir sınıfı "override" (ezme/yerine koyma) işlemi ile kullanabiliriz. UVM'in yeniden kullanılabilirlik (reusability) gücünün kalbi bu makrodur.

Soru 2: Nesneleri yaratırken SystemVerilog'un kendi yöntemi olan new() metodunu kullanmak varken, neden type_id::create() gibi uzun bir yazım kullanıyoruz?

Cevap: Eğer my_agent = new() yazarsanız, o değişkene kesinlikle ve sadece my_agent sınıfını atamış olursunuz. Yani donanıma sert bir şekilde lehimlemişsiniz gibi düşünün.

Ancak my_agent::type_id::create() yazdığınızda, arka planda UVM Factory'ye şu soruyu sorarsınız: "Bana bir my_agent lazım, ama acaba testin başka bir yerinde bu sınıf yerine başka bir özel sınıf (örneğin my_error_agent) kullanmam istendi mi?" Factory kontrolü yapar ve size doğru olan nesneyi verir. create() kullanmak kodunuzu esnek, dinamik ve gelecekteki değişikliklere açık hale getirir.

Soru 3: Neden build_phase bir function (fonksiyon) olarak tanımlanmışken, run_phase bir task (görev) olarak tanımlanmış?

Cevap: SystemVerilog'da function'lar simülasyon zamanı (time) harcayamazlar; anında (0 delta time) çalışıp bitmek zorundadırlar. task'ler ise içinde #10, @(posedge clk), wait() gibi zaman geçiren ifadeler barındırabilir.

Test ortamı kurulurken (nesnelerin yaratılması, bağlanması vs.) zaman akmamalıdır, her şey simülasyonun 0. anından önce hazır olmalıdır. Bu yüzden build_phase ve connect_phase birer function'dır.

Ancak run_phase simülasyonun asıl aktığı, verilerin gönderildiği ve saat vuruşlarının beklendiği yerdir. Bu yüzden zaman harcayabilen bir task olmak zorundadır.

Soru 4: build_phase yukarıdan aşağıya (Top-Down) çalışırken, neden connect_phase aşağıdan yukarıya (Bottom-Up) çalışıyor?

Cevap: Bu tamamen mantıksal bir zorunluluktur. Bir şeyin alt bileşenlerini yaratmak için önce o şeyi yaratmanız gerekir. Yani önce env yaratılmalı ki, onun içindeki agent yaratılabilsin. Bu yüzden build_phase yukarıdan aşağıya inmelidir (Test -> Env -> Agent).

Bağlantı (Connect) aşamasında ise, en alt seviyedeki arayüzlerin (interface) veya portların bağlandığından emin olup, sorunsuz olduklarını gördükten sonra üst hiyerarşideki bağlantıları yapmak daha güvenlidir. Bu nedenle connect_phase içeriden dışarıya (aşağıdan yukarıya) doğru ilerler.

Soru 5: Kodun içinde phase.raise_objection() ve drop_objection() gördük. Bunları yazmazsak simülasyon çöker mi?

Cevap: Çökmez, ancak simülasyon 0 anında biter ve hiçbir test senaryosu çalışmaz.

UVM'de simülasyonun ne zaman biteceğine karar veren mekanizma "Objection" (İtiraz) mekanizmasıdır. UVM tüm bileşenlere şunu sorar: "Simülasyonu bitiriyorum, itirazı olan var mı?". Eğer bir test veya bileşen raise_objection demezse, UVM simülasyonu anında sonlandırır. drop_objection ise "Benim işim bitti, itirazımı geri çekiyorum, istersen simülasyonu kapatabilirsin" demektir.

Soru 6: Raporlama kısmında UVM_LOW, UVM_MEDIUM, UVM_HIGH gibi seviyeler var. Neden sadece "yazdır" deyip geçmiyoruz?

Cevap: Büyük bir projede milyonlarca log mesajı olabilir. Eğer her mesaj ekrana yazdırılırsa simülasyon inanılmaz derecede yavaşlar ve asıl hatayı bulmanız imkansız hale gelir.

Bu "verbosity" (detay) seviyeleri mesajları filtrelememizi sağlar.

  • UVM_LOW: Çok önemli mesajlar. Her zaman yazdırılır.
  • UVM_MEDIUM: Varsayılan seviye. Normal test akışı.
  • UVM_HIGH: Daha detaylı bilgi, veri paketlerinin içerikleri gibi. Sadece istenirse yazdırılır.
  • UVM_DEBUG: Sadece hata ararken (debugging) görmek isteyeceğiniz en ince detaylar.
    Simülatöre verilen bir parametre ile hangi seviyenin altının ekrana basılacağına dinamik olarak karar verebiliriz.

Soru 7: Metotların içinde hep super.build_phase(phase) yazıyoruz. Kendi kodumuzu zaten yazıyoruz, bunu silsek olmaz mı?

Cevap: Bunu silmek tehlikelidir ve tavsiye edilmez. Sınıfımız (örneğin base_test) uvm_test isimli UVM ana sınıfından miras alır. UVM'in kendi kütüphanesindeki o üst sınıfta, arka planda çalışan önemli konfigürasyon ayarları veya kayıt işlemleri olabilir. super.build_phase(phase) diyerek "Önce benim miras aldığım UVM babasının build işlemleri çalışsın, sonra benim kendi yazdığım kodlar çalışsın" garantisini veriyoruz. Silerseniz arka plandaki konfigürasyon mekanizmalarını bozabilirsiniz.

Soru 8: Test ağacını (Test, Env, Agent) kurduk ama ortada test edilecek bir donanım (DUT) modülü yok. Bu kod şu an neyi test ediyor?

Cevap: Şu an hiçbir şeyi test etmiyor! 1. günün amacı donanımı değil, doğrulama altyapısının iskeletini ayağa kaldırmaktır. Önce evimizin temelini ve odalarını (Env, Agent, Test) inşa ediyoruz. İlerleyen günlerde, donanımı (DUT) temsil eden modülü getireceğiz, sinyalleri (Interface) tanımlayacağız ve Agent'ımızın içindeki bileşenlerle bu donanımı sürmeye (Driver) ve izlemeye (Monitor) başlayacağız.