EDA Playground'da Dene

UVM Sequence'ler ve uvm_config_db Kullanımı

UVM Gün 4: Sequence'lar, Stimulus Yönetimi ve Konfigürasyon | uvm_config_db set/get, uvm_sequence, start_item/finish_item el sıkışması, hiyerarşik sequence'ler, objection yönetimi

Amaç

Bu laboratuvarın amacı, UVM testbench'ine dinamik senaryolar eklemek için Sequence sınıfları (uvm_sequence) oluşturmak, hiyerarşik (iç içe) senaryolar yazmak ve UVM Configuration Database (uvm_config_db) kullanarak testten en alt seviyedeki bileşenlere (örneğin Driver'a) parametre/konfigürasyon aktarımı yapmaktır.

Ön Koşullar

  • 1., 2. ve 3. gün konuları (Agent hiyerarşisi, Sequence Item, TLM).
  • SystemVerilog rand değişkenler ve with kısıtları (constraints) hakkında bilgi.

Lab 4.1: UVM Configuration DB Kullanımı

uvm_config_db, testbench'in herhangi bir yerinden bir kutuya bilgi koyup (set), başka bir yerinden bu kutudaki bilgiyi güvenle almamızı (get) sağlayan sihirli bir veri tabanıdır. Genellikle sanal arayüzleri (virtual interface) veya konfigürasyon nesnelerini taşımak için kullanılır.

uvm_config_db Fonksiyonları ve Parametreleri

Veri tabanına bir şey yazmak için set(), okumak için ise get() metodu kullanılır. Her iki metot da temel olarak 4 adet parametre alır:

Sözdizimi (Syntax):

uvm_config_db#(TYPE)::set(context, inst_name, field_name, value);

uvm_config_db#(TYPE)::get(context, inst_name, field_name, value);

  • #(TYPE) : Taşınacak verinin tipidir. int, string, virtual interface veya konfigürasyon sınıfı (uvm_object) olabilir.
  • context (Bağlam): İşlemin yapıldığı referans noktasıdır. Genellikle veriyi gönderen/alan sınıfın kendisi olan this anahtar kelimesi kullanılır. Global bir gönderim için uvm_root::get() veya null kullanılabilir.
  • inst_name (Örnek Adı): Hedefin, context'e göre hiyerarşik yoludur.
    • Gönderirken (Set): "env.agent.driver" gibi hedefin tam veya göreceli adresi yazılır.
    • Alırken (Get): Sınıf zaten hedefin kendisiyse, "benim içimde ara" manasına gelen boş string "" kullanılır.
  • field_name (Alan Adı): Bu bilgiye verdiğimiz özel etikettir (Örn: "timeout_degeri", "vif"). Okuyan taraf, tam olarak bu etiketi arayarak veriyi bulur.
  • value (Değer): set işleminde kutuya konulan asıl veri, get işleminde ise kutudan çıkan verinin kaydedileceği değişkendir.

Basit Kullanım Örnekleri:

// 1. Passing a simple integer  
uvm_config_db#(int)::set(this, "*", "max_loops", 100); 

// 2. Passing a string globally  
uvm_config_db#(string)::set(null, "*", "greeting", "Hello UVM!");

// 3. Receiving a string in any component  
string my_str;  
if (uvm_config_db#(string)::get(this, "", "greeting", my_str))  
  `uvm_info("CFG", $sformatf("Got string: %s", my_str), UVM_LOW)

Kodun Açıklaması

Bu blok, uvm_config_db'nin en temel set ve get kullanımlarını üç küçük örnekle gösterir:

  • 1. Örnek (uvm_config_db#(int)::set): Tip parametresi olarak int verilir ve veri tabanına "max_loops" etiketiyle 100 değeri yazılır. context olarak this, inst_name olarak ise "*" (joker karakter) kullanılmıştır. "*" ifadesi "bu bağlamın altındaki tüm bileşenler bu değere erişebilsin" anlamına gelir; yani değerin görünürlüğünü olabildiğince geniş tutar.
  • 2. Örnek (uvm_config_db#(string)::set): Bu kez tip string'tir ve context olarak null verilmiştir. null bağlam, atamayı global (en tepeden) yapar; böylece "greeting" etiketli "Hello UVM!" değeri testbench'in herhangi bir noktasından okunabilir.
  • 3. Örnek (uvm_config_db#(string)::get): Önce değeri tutacak bir my_str değişkeni tanımlanır. get metodu okuma başarılı olursa 1, başarısız olursa 0 döndürdüğü için çağrı bir if koşulu içine alınmıştır. inst_name olarak boş string "" kullanılması "tam olarak bu bileşene gönderilen değeri ara" demektir. Değer bulunursa uvm_info makrosu ile $sformatf kullanılarak okunan metin ekrana basılır.

Bu adımda, Test sınıfından Driver sınıfına bir "gecikme süresi" (delay_time) parametresi aktaracağız.

Görev 1: base_test sınıfının build_phase metodunda bir uvm_config_db ataması (set) yapın.

class base_test extends uvm_test;  
  `uvm_component_utils(base_test)  
    
  my_env env_inst;  
    
  // Constructor  
  function new(string name = "base_test", uvm_component parent = null);  
    super.new(name, parent);  
  endfunction  
    
  virtual function void build_phase(uvm_phase phase);  
    super.build_phase(phase);  
    env_inst = my_env::type_id::create("env_inst", this);  
      
    // Set a configuration integer for the driver's delay  
    // Context: this (base_test)  
    // Target Path: "env_inst.agent_inst.drv"  
    // Key/Field: "driver_delay"  
    // Value: 35  
    uvm_config_db#(int)::set(this, "env_inst.agent_inst.drv", "driver_delay", 35);  
      
    `uvm_info("TEST_CFG", "Set driver_delay to 35 via config_db.", UVM_LOW)  
  endfunction  
    
  // ... (Other phases will be added later)  
endclass

Kodun Açıklaması

Bu blok, konfigürasyon değerini gönderen (set eden) tarafı tanımlar:

  • uvm_component_utils(base_test): Sınıfı UVM fabrikasına (factory) kaydeder; böylece base_test ismiyle dinamik olarak yaratılabilir ve type_id::create ile örneklenebilir.
  • function new(...): Bileşenin kurucusudur (constructor). super.new(name, parent) çağrısıyla üst sınıfın kurucusunu çalıştırarak bileşeni UVM hiyerarşisine parent altına bağlar.
  • build_phase: Bileşenlerin yukarıdan aşağıya (top-down) yaratıldığı fazdır. İlk olarak super.build_phase(phase) çağrılır.
  • my_env::type_id::create("env_inst", this): env nesnesini fabrika üzerinden yaratır. İkinci argüman this ile yaratılan env_inst'in base_test'in çocuğu olduğu belirtilir.
  • uvm_config_db#(int)::set(this, "env_inst.agent_inst.drv", "driver_delay", 35): Asıl konfigürasyon atamasıdır. context olarak this (yani base_test), inst_name olarak hedefe giden hiyerarşik yol "env_inst.agent_inst.drv", field_name olarak "driver_delay" etiketi ve value olarak 35 verilir. Bu satır, "ileride bu yolda yaratılacak olan driver, driver_delay etiketiyle 35 değerini bulsun" anlamına gelir.
  • uvm_info("TEST_CFG", ...): Atamanın yapıldığını UVM_LOW ayrıntı seviyesinde loglar.

Görev 2: 2. Gün yazdığımız my_driver sınıfının build_phase metodunu güncelleyerek bu değeri okuyun (get).

class my_driver extends uvm_driver #(my_transaction);  
  `uvm_component_utils(my_driver)  
    
  // Variable to hold the delay time  
  int delay_time = 10; // Default value if not set by config_db  
    
  // Constructor  
  function new(string name = "my_driver", uvm_component parent = null);  
    super.new(name, parent);  
  endfunction  
    
  virtual function void build_phase(uvm_phase phase);  
    super.build_phase(phase);  
      
    // Get the configuration integer  
    // Context: this (my_driver)  
    // Target Path: "" (Look exactly at this component)  
    // Key/Field: "driver_delay"  
    // Variable to store: delay_time  
    if(uvm_config_db#(int)::get(this, "", "driver_delay", delay_time)) begin  
      `uvm_info("DRV_CFG", $sformatf("Successfully got driver_delay: %0d", delay_time), UVM_LOW)  
    end else begin  
      `uvm_warning("DRV_CFG", "Could not get driver_delay! Using default.")  
    end  
  endfunction  
    
  virtual task run_phase(uvm_phase phase);  
    super.run_phase(phase);  
    forever begin  
      seq_item_port.get_next_item(req);  
      `uvm_info("DRIVER", $sformatf("Driving Transaction: Addr=0x%0h, WE=%0b", req.addr, req.write_en), UVM_LOW)  
        
      // Use the dynamically configured delay time  
      #(delay_time);   
        
      seq_item_port.item_done();  
    end  
  endtask  
endclass

Kodun Açıklaması

Bu blok, konfigürasyon değerini alan (get eden) ve onu kullanan tarafı tanımlar:

  • int delay_time = 10;: Okunacak değeri tutacak değişkendir. 10 varsayılan değeri, config_db'den okuma başarısız olursa devreye girer; böylece simülasyon yine de güvenli bir değerle devam eder.
  • build_phase içindeki uvm_config_db#(int)::get(this, "", "driver_delay", delay_time): set ile gönderilen değeri okur. context olarak this (driver'ın kendisi), inst_name olarak boş string "" ("tam olarak bana gönderileni ara") ve field_name olarak "driver_delay" kullanılır. Okunan değer doğrudan delay_time değişkenine yazılır.
  • if (...) ... else ...: get başarılı olursa 1 döndürür ve uvm_info ile okunan değer loglanır; başarısız olursa uvm_warning ile uyarı basılır ve varsayılan değerle devam edilir. Bu savunmacı yaklaşım, eksik konfigürasyonun sessizce gözden kaçmasını engeller.
  • run_phase içindeki seq_item_port.get_next_item(req): Sequencer'dan sıradaki transaction'ı (req) bloklayarak çeker; transaction gelene kadar bekler.
  • #(delay_time): Sürme işlemi sırasında config_db'den dinamik olarak okunan gecikmeyi uygular. Sabit bir değer yerine konfigüre edilebilir delay_time kullanılması, aynı driver'ın farklı testlerde farklı zamanlamalarla çalışmasını sağlar.
  • seq_item_port.item_done(): Driver'ın işlemi tamamladığını sequencer'a (ve dolayısıyla finish_item'da bekleyen sequence'a) bildirir; handshake'i kapatır.

En Yaygın Kullanım: Virtual Interface Taşımak

uvm_config_db'nin gerçek projelerdeki bir numaralı görevi, top modüldeki fiziksel interface'i sınıf dünyasındaki driver/monitor'e ulaştırmaktır. SystemVerilog projesinde bunu new(aif, aif) ile yapıcıya vererek çözmüştünüz; UVM'de yapıcı imzası sabit olduğu için (new(name, parent)) bu yol kapalıdır ve config_db devreye girer:

// tb_top (modul): interface'i veritabanina koy
alu_if aif(clk);
initial begin
  uvm_config_db#(virtual alu_if)::set(null, "uvm_test_top.*", "vif", aif);
  run_test();
end

// my_driver::build_phase (sinif): interface'i al, yoksa oldurucu hata
virtual alu_if vif;
if (!uvm_config_db#(virtual alu_if)::get(this, "", "vif", vif))
  `uvm_fatal("NOVIF", "Virtual interface config_db'de bulunamadi!")

set çağrısındaki null bağlam "en tepeden" demektir; "uvm_test_top.*" ise test altındaki her bileşenin bu anahtarı görebilmesini sağlar. get başarısızlığında `uvm_fatal kullanmak standarttır: vif olmadan driver'ın çalışması imkânsızdır, sessizce devam etmek yalnızca daha geç ve daha anlaşılmaz bir hata üretir.

config_db Adresleme Kuralları

Parametre set tarafında get tarafında
context Genellikle this (test/env) veya null (top modül) Her zaman this
inst_name Hedefin context'e göre yolu: "env_inst.agent_inst.drv", joker: "*", "env_inst.*" Genellikle "" (kendim)
field_name Anahtar string Aynı string
Tip parametresi #(T) Taşınan tip Birebir aynı tip (int ≠ int unsigned!)

Eşleşme kuralı: get yapan bileşenin tam yolu (uvm_test_top.env_inst.agent_inst.drv), set'teki context + inst_name birleşiminin ürettiği desenle eşleşmelidir. Birden fazla set eşleşirse hiyerarşide daha yukarıdan yapılan set kazanır (test, env'i ezer); aynı seviyedeyse son yapılan kazanır. Bu kural, testin environment'ın varsayılanlarını ezebilmesini sağlar.

Lab 4.2: Temel Sequence'ların Oluşturulması

Sequencer ve Driver artık komut bekliyor. Modern UVM'de senaryoları (sequence) yazarken doğrudan start_item() ve finish_item() API'leri kullanılır.

Önemli Kavram: start_item ve finish_item Nasıl Çalışır? (Handshake Mekanizması)

Bu iki metot, Sequence (Garson), Sequencer (Sipariş Sırası) ve Driver (Aşçı) arasındaki senkronizasyonu sağlar:

  1. start_item(req) (İzin İsteme): Sequence, "Elimde yeni bir işlem var, Driver müsait mi?" diye sorar. Driver get_next_item() komutunu çağırana kadar bu satır kodun akışını bloklar (bekletir).
  2. Late Randomization (Geç Rastgeleleştirme): Dikkat ederseniz koddaki req.randomize() işlemi start_item'dan sonra yapılmıştır. Bunun sebebi veriyi tam Driver'ın müsait olduğu (iznin çıktığı) milisaniyede üretmektir. Böylece veri, simülasyonun o anki en güncel durumuna göre şekillenir.
  3. finish_item(req) (Teslimat ve Onay Bekleme): İçi doldurulmuş paket Driver'a yollanır. Ancak Sequence işine devam etmez; Driver fiziksel sürme işlemini bitirip item_done() çağrısını yapana kadar bu satırda bekler.

Görev: Bu mekanizmayı kullanarak; biri sadece YAZMA işlemi üreten (write_sequence), diğeri sadece OKUMA işlemi üreten (read_sequence) iki temel senaryo yazın.

// ----------------------------------------------------------------------------  
// SEQUENCE 1: Write Sequence  
// ----------------------------------------------------------------------------  
class write_sequence extends uvm_sequence #(my_transaction);  
  `uvm_object_utils(write_sequence)  
    
  function new(string name = "write_sequence");  
    super.new(name);  
  endfunction  
    
  // The 'body' task contains the behavior of the sequence  
  virtual task body();  
    // Create the transaction object  
    req = my_transaction::type_id::create("req");  
      
    // Step 1: Wait for driver to be ready (Handshake begins)  
    start_item(req);  
      
    // Step 2: Late Randomization with inline constraints  
    if (!req.randomize() with { write_en == 1'b1; }) begin  
      `uvm_error("SEQ", "Randomization failed!")  
    end  
      
    // Step 3: Send to driver and wait for item_done (Handshake completes)  
    finish_item(req);  
  endtask  
endclass

// ----------------------------------------------------------------------------  
// SEQUENCE 2: Read Sequence  
// ----------------------------------------------------------------------------  
class read_sequence extends uvm_sequence #(my_transaction);  
  `uvm_object_utils(read_sequence)  
    
  function new(string name = "read_sequence");  
    super.new(name);  
  endfunction  
    
  virtual task body();  
    req = my_transaction::type_id::create("req");  
    start_item(req);  
    if (!req.randomize() with { write_en == 1'b0; }) begin  
      `uvm_error("SEQ", "Randomization failed!")  
    end  
    finish_item(req);  
  endtask  
endclass

Kodun Açıklaması

Bu blok, iki temel senaryoyu (write_sequence ve read_sequence) tanımlar. İkisi de uvm_sequence #(my_transaction) sınıfından türer:

  • uvm_object_utils(...): Sequence'lar bileşen değil nesne (object) olduğu için uvm_component_utils yerine uvm_object_utils ile fabrikaya kaydedilir.
  • function new(string name = ...): Sequence kurucusu yalnızca bir name argümanı alır; çünkü nesnelerin hiyerarşik bir parent'ı yoktur (bileşenlerin aksine).
  • virtual task body();: Sequence'ın asıl davranışını içeren görevdir. Sequence başlatıldığında otomatik olarak body çalışır.
  • req = my_transaction::type_id::create("req"): Gönderilecek transaction nesnesi fabrika üzerinden yaratılır.
  • start_item(req): Handshake'i başlatır; driver get_next_item çağırıp müsait olana kadar bu satır akışı bloklar.
  • req.randomize() with { write_en == 1'b1; }: Geç rastgeleleştirme (late randomization) adımıdır ve start_item'dan sonra yapılır. with bloğundaki satır içi kısıt (inline constraint), write_sequence'te write_en'i 1 (yazma), read_sequence'te ise 0 (okuma) olmaya zorlar. randomize() başarısız olursa 0 döndürür; bu durumda uvm_error ile hata basılır.
  • finish_item(req): Doldurulmuş transaction'ı driver'a teslim eder ve driver item_done çağırana kadar bekleyerek handshake'i tamamlar.

İki sequence arasındaki tek anlamlı fark, randomize() with içindeki kısıttır (write_en == 1'b1 yazma için, write_en == 1'b0 okuma için).

uvm_do Makro Ailesi ile Karşılaştırma

Eski kodlarda ve bazı kitaplarda aynı iş makrolarla yazılır. Hangi makronun hangi API çağrılarına karşılık geldiğini bilmek, o kodu okumanız için yeterlidir:

Makro Eşdeğer API Not
`uvm_do(req) create + start_item + randomize + finish_item randomize başarısızlığını yalnızca uvm_warning ile geçiştirir
`uvm_do_with(req, { write_en == 1; }) Aynısı, randomize() with {...} Bu dersteki write_sequence::body
`uvm_create(req) + `uvm_send(req) create … start_item + finish_item (randomize yok) Alanları elle doldurduğunuz yönlendirilmiş paketler
`uvm_do_on(req, sqr) / `uvm_do_on_with Aynısı, belirtilen sequencer'da Virtual sequence'lerde (Gün 8)
`uvm_do_pri(req, 500) start_item(req, 500) … Öncelikli istek (Gün 8, arbitrasyon)
`uvm_do(sub_seq) sub_seq.start(m_sequencer, this) Argüman bir sequence ise makro bunu anlar

Açık API'yi tercih etmemizin sebepleri FAQ'daki Soru 2'de: hata ayıklamada satır satır görünürlük, randomize dönüşünü kendi kurallarınızla ele almak ve start_item ile finish_item arasına kendi kodunuzu (geç rastgeleleştirme, log, bekleme) koyabilmek. Bir projede iki stili karıştırmayın; okuyan kişi her satırda "bu makro ne yapıyordu?" diye durmak zorunda kalır.

Lab 4.3: Hiyerarşik (Master) Sequence Oluşturma

Birden fazla alt senaryoyu tek bir çatı altında toplayarak karmaşık senaryolar yaratabiliriz. Buna "Hierarchical Sequences" denir.

Görev: 2 kez yazma, ardından 2 kez okuma yapan bir ana sequence (main_sequence) oluşturun.

class main_sequence extends uvm_sequence #(my_transaction);  
  `uvm_object_utils(main_sequence)  
    
  // Handles for sub-sequences  
  write_sequence wr_seq;  
  read_sequence  rd_seq;  
    
  function new(string name = "main_sequence");  
    super.new(name);  
  endfunction  
    
  virtual task body();  
    `uvm_info("MAIN_SEQ", "Starting main hierarchical sequence...", UVM_LOW)  
      
    // Execute 2 Write sequences  
    repeat(2) begin  
      wr_seq = write_sequence::type_id::create("wr_seq");  
      // To start a sub-sequence, pass the current sequencer (m_sequencer)  
      wr_seq.start(m_sequencer, this);  
    end  
      
    // Execute 2 Read sequences  
    repeat(2) begin  
      rd_seq = read_sequence::type_id::create("rd_seq");  
      rd_seq.start(m_sequencer, this);  
    end  
      
    `uvm_info("MAIN_SEQ", "Main sequence finished.", UVM_LOW)  
  endtask  
endclass

Kodun Açıklaması

Bu blok, alt sequence'ları çağıran hiyerarşik (ana) sequence'ı tanımlar:

  • write_sequence wr_seq; ve read_sequence rd_seq;: Alt senaryolara ait tutamaçlar (handle) sınıf içinde tanımlanır.
  • body() içindeki repeat(2) begin ... end: Aynı işlemin iki kez tekrarlanmasını sağlar; önce iki yazma, ardından iki okuma üretilir.
  • wr_seq = write_sequence::type_id::create("wr_seq"): Her tekrarda alt sequence fabrika üzerinden yeniden yaratılır. Döngü içinde yeniden yaratmak, her transaction'ın taze bir nesne olmasını sağlar.
  • wr_seq.start(m_sequencer, this): Alt sequence'ı çalıştıran kritik satırdır. start metodu alt sequence'ın body'sini tetikler. Birinci argüman m_sequencer, ana sequence'ın hâlihazırda üzerinde koştuğu sequencer'dır; bu sayede alt sequence'lar ayrıca bir sequencer'a atanmadan aynı sequencer üzerinde çalışır. İkinci argüman this, alt sequence'ın "parent sequence"ını belirtir ve hiyerarşik ilişkiyi (parent/child) kurarak ilerleme/öncelik yönetimini düzgün tutar.

m_sequencer, sequence başlatıldığında UVM tarafından otomatik olarak ayarlanan, sequence'ın bağlı olduğu sequencer'a işaret eden yerleşik bir değişkendir; bu yüzden alt sequence'ları aynı sequencer'a yönlendirmek için onu doğrudan kullanabiliriz.

Lab 4.4: Sequence'ın Test İçinden Tetiklenmesi

Artık hiyerarşik senaryomuzu test sınıfının (base_test) run_phase'i içerisinde tetikleyebiliriz. Simülasyon süresini kontrol etmek için objection (raise_objection / drop_objection) mekanizmasını kullanmayı unutmayacağız.

Görev: base_test sınıfını güncelleyerek main_sequence'i Agent'ın içindeki Sequencer üzerinde başlatın.

class base_test extends uvm_test;  
  `uvm_component_utils(base_test)  
    
  my_env env_inst;  
    
  function new(string name = "base_test", uvm_component parent = null);  
    super.new(name, parent);  
  endfunction  
    
  virtual function void build_phase(uvm_phase phase);  
    super.build_phase(phase);  
    env_inst = my_env::type_id::create("env_inst", this);  
    uvm_config_db#(int)::set(this, "env_inst.agent_inst.drv", "driver_delay", 35);  
  endfunction  
    
  virtual task run_phase(uvm_phase phase);  
    // Declare the sequence  
    main_sequence m_seq;  
      
    // 1. Raise objection to start simulation time  
    phase.raise_objection(this);  
      
    `uvm_info("TEST", "Starting main sequence...", UVM_LOW)  
      
    // 2. Create the sequence  
    m_seq = main_sequence::type_id::create("m_seq");  
      
    // 3. Start the sequence on the designated sequencer  
    m_seq.start(env_inst.agent_inst.sqr);  
      
    `uvm_info("TEST", "Main sequence completed.", UVM_LOW)  
      
    // Add a little extra time before shutting down  
    #50;  
      
    // 4. Drop objection to end simulation  
    phase.drop_objection(this);  
  endtask  
endclass

Kodun Açıklaması

Bu blok, hem konfigürasyon atamasını (build_phase) hem de senaryonun tetiklenmesini (run_phase) bir arada gösterir:

  • build_phase: Önceki gibi env_inst'i type_id::create ile yaratır ve uvm_config_db#(int)::set(...) ile driver'a driver_delay = 35 değerini gönderir.
  • run_phase içindeki main_sequence m_seq;: Çalıştırılacak ana sequence için bir tutamaç tanımlanır.
  • phase.raise_objection(this): Simülasyon zamanının erken bitmesini engeller. Objection kaldırıldığı sürece UVM, bu fazın hâlâ işi olduğunu bilir ve simülasyonu sonlandırmaz.
  • m_seq = main_sequence::type_id::create("m_seq"): Ana sequence nesnesi fabrika üzerinden yaratılır.
  • m_seq.start(env_inst.agent_inst.sqr): Sequence'ı, agent'ın içindeki sequencer (sqr) üzerinde başlatır. Burada start'a sequencer'ın tam hiyerarşik referansı verilir; bu, sequence'ı doğru sequencer'a bağlar ve body'sinin çalışmasını başlatır. Sequence tamamen bitene kadar bu satır bekler.
  • #50;: Son transaction sürüldükten sonra sinyallerin yerleşmesi için simülasyona biraz ek süre tanır.
  • phase.drop_objection(this): Objection'ı düşürür. Bekleyen başka objection kalmadığında UVM run_phase'i sonlandırır ve simülasyon temiz bir şekilde biter.

Lab 4.5: Konfigürasyon Nesnesi (Config Object) Kalıbı

driver_delay için bir set, is_active için bir set, coverage açık mı diye bir set… Bileşen başına parametre sayısı arttıkça uvm_config_db çağrıları dağılır, tip hataları çoğalır ve "varsayılan değer nerede?" sorusunun cevabı kaybolur. Sektördeki standart çözüm, bir bileşenin tüm ayarlarını tek bir uvm_object'te toplamak ve veritabanından bir kez geçirmektir.

Görev: my_agent için bir konfigürasyon sınıfı yazın; test onu bir kez set etsin, agent bir kez get edip alt bileşenlerine handle ile dağıtsın.

// Agent'in tum ayarlari tek nesnede; varsayilanlar burada
class my_agent_config extends uvm_object;
  `uvm_object_utils(my_agent_config)

  uvm_active_passive_enum is_active    = UVM_ACTIVE;
  int                     driver_delay = 10;
  bit                     has_coverage = 1;

  function new(string name = "my_agent_config");
    super.new(name);
  endfunction
endclass

// base_test::build_phase — test yalnizca farkli olani degistirir
virtual function void build_phase(uvm_phase phase);
  my_agent_config cfg;
  super.build_phase(phase);
  env_inst = my_env::type_id::create("env_inst", this);
  cfg = my_agent_config::type_id::create("cfg");
  cfg.driver_delay = 35;                       // digerleri varsayilan kalir
  uvm_config_db#(my_agent_config)::set(this, "env_inst.agent_inst", "cfg", cfg);
endfunction

// my_agent::build_phase — bilesen SINIRINDA bir kez get, iceride handle
my_agent_config cfg;

virtual function void build_phase(uvm_phase phase);
  super.build_phase(phase);
  if (!uvm_config_db#(my_agent_config)::get(this, "", "cfg", cfg))
    `uvm_fatal("NOCFG", "my_agent_config bulunamadi")
  mon = my_monitor::type_id::create("mon", this);
  if (cfg.is_active == UVM_ACTIVE) begin
    sqr = my_sequencer::type_id::create("sqr", this);
    drv = my_driver::type_id::create("drv", this);
    drv.cfg = cfg;                              // config_db DEGIL: duz handle atamasi
  end
endfunction

// my_driver — cfg handle'i agent'tan gelir; build_phase'den once atanmistir
my_agent_config cfg;
virtual task drive_item(my_transaction tr);
  `uvm_info("DRIVER", tr.convert2string(), UVM_LOW)
  #(cfg.driver_delay);
endtask

Kodun Açıklaması

  • Varsayılanlar sınıfta, farklar testte: cfg.driver_delay = 35 dışında hiçbir şey yazılmadı; is_active ve has_coverage varsayılan değerleriyle gider. Yeni bir ayar eklemek, sınıfa bir alan eklemektir; hiçbir set/get çağrısı değişmez.
  • uvm_config_db#(my_agent_config): Tip parametresi artık kendi sınıfınızdır; int ↔ bit [31:0] türünden sessiz eşleşmeme hataları (Sık Yapılan Hatalar) yok olur: ya nesne gelir ya `uvm_fatal.
  • Bileşen sınırında get, içeride handle: Agent'ın altındaki driver ve monitor uvm_config_db'ye hiç bakmaz; drv.cfg = cfg ile nesneyi alır. drv.cfg, driver'ın build_phase'i çalışmadan önce atanmıştır (build top-down: agent'ın build_phase'i biter, sonra çocuklarınki başlar). Veritabanı aramaları string eşleştirmedir ve ucuz değildir; yüzlerce bileşenin her alanı ayrı ayrı araması simülasyon başlangıcını gözle görülür yavaşlatır.
  • is_active nereden geliyor? uvm_agent'ın kendi is_active alanı da vardır ve uvm_config_db#(uvm_active_passive_enum) ile ayarlanabilir (Gün 2). Config nesnesi kullanan ekipler tek kaynak ilkesiyle cfg.is_active'i okur; karışık kullanmayın.
  • Nesne, kopya değil handle olarak taşınır: Test set'ten sonra cfg.driver_delay'i değiştirirse driver bunu görür (çünkü aynı nesne). Bu bir özellik olabilir (test ortasında ayar değiştirmek) ya da tuzak; kasıtlı kullanın.

Sequence'ten Konfigürasyon Okumak

Sequence'ler bileşen değildir: hiyerarşide yolları yoktur, this ile get yapamazlar. Ama bir sequencer üzerinde koştukları için sequencer'ın bağlamını ödünç alabilirler:

// Test: sequencer'in yoluna yaz
uvm_config_db#(int)::set(this, "env_inst.agent_inst.sqr", "n_items", 50);

// Sequence: m_sequencer baglamiyla oku (start() sonrasi dolu)
virtual task body();
  int n_items = 10;
  if (!uvm_config_db#(int)::get(m_sequencer, "", "n_items", n_items))
    `uvm_info("SEQ", "n_items verilmedi, varsayilan 10", UVM_LOW)
  repeat (n_items) begin
    // ... start_item / finish_item ...
  end
endtask

m_sequencer, start() çağrısıyla dolar; bu yüzden get body() içinde (yapıcıda değil) yapılır. Alternatif, Gün 8'de göreceğiniz p_sequencer üzerinden sequencer'a konmuş bir config nesnesine ulaşmaktır. Sequence'lerin kendi uvm_config_db#(...)::set yapmasından kaçının; konfigürasyon yukarıdan aşağı akar, senaryodan yukarı değil.

Önemli Noktalar

Bu dersin tamamından çıkarılması gereken en kritik püf noktaları ve en iyi pratikler şunlardır:

  • String tabanlı adresleme: uvm_config_db ile set yaparken hedefi ("env_inst.agent_inst.drv" gibi) string olarak yazın; çünkü build_phase top-down çalışır ve değer gönderilirken hedef bileşen henüz yaratılmamış olabilir. Geniş erişim için "*" jokerini kullanmaktan çekinmeyin.
  • get dönüş değerini her zaman kontrol edin: get çağrısını bir if içine alıp başarısız durumu (uvm_warning + varsayılan değer) ele alın. Bu, eksik veya hatalı yazılmış bir field_name'in sessizce gözden kaçmasını engeller.
  • Geç rastgeleleştirme (late randomization): randomize()'ı her zaman start_item'dan sonra, finish_item'dan önce yapın. Böylece veri, driver'ın gerçekten hazır olduğu en güncel simülasyon durumuna göre üretilir.
  • Doğru fabrika makrosunu seçin: Bileşenler (uvm_component) için uvm_component_utils, sequence'lar ve transaction'lar (uvm_object) için uvm_object_utils kullanın. Tüm nesneleri new yerine type_id::create ile yaratarak fabrika override'larından faydalanın.
  • m_sequencer ile yeniden kullanım: Hiyerarşik sequence'larda alt sequence'ları start(m_sequencer, this) ile başlatın; bu, alt sequence'ları ana sequence ile aynı sequencer'a bağlar ve parent/child ilişkisini kurar.
  • Objection'ı yalnızca testte yönetin: Simülasyonun başlangıç/bitiş kontrolünü (raise_objection / drop_objection) her zaman testin en üst run_phase'inde yapın; performans nedeniyle bunu tek tek sequence'lar içinde kullanmaktan kaçının.

Sequence–Sequencer–Driver El Sıkışması: Zaman Çizelgesi

 Sequence (body)             Sequencer                 Driver (run_phase)
 ───────────────             ─────────                 ──────────────────
 req = create("req")
 start_item(req)  ──────────▶ kuyruga al,
     (bekler)                 arbitrasyon   ◀────────── get_next_item(req)   (bekliyordu)
     ◀────────────────────── izin ver ────────────────▶ req teslim
 req.randomize() with {...}   (gec rastgelelestirme: driver hazir, veri simdi uretilir)
 finish_item(req) ──────────▶ driver'a ilet
     (bekler)                                            #delay_time  (pinleri sur)
     ◀────────────────────── item_done() ◀───────────── item_done()
 (devam)                                                 get_next_item(req) (yeni tur)

İki bekleme noktası vardır ve ikisi de bilinçlidir: start_item driver hazır olana kadar, finish_item driver işi bitirene kadar bekler. Böylece sequence hiçbir zaman driver'dan "ileri kaçamaz" ve bir sonraki paketi, bir öncekinin gerçekten sürüldüğünü bilerek üretir. SystemVerilog projesindeki mailbox bu garantiyi vermiyordu: generator 100 paketi mailbox'a yığıp bitebilirdi.

Sequence Katmanı: SystemVerilog Projesiyle Karşılaştırma

SV projesi (Gün 6–7) UVM (bu ders)
ALU_Generator::run() içinde for döngüsü uvm_sequence::body()
ALU_Corner_Generator extends ALU_Generator Farklı uvm_sequence sınıfları (write_sequence, read_sequence)
env.gen = corner_gen ile senaryo seçimi Test'te seq.start(sqr) ile hangi sequence'in koşacağı seçilir; factory override ile de değiştirilebilir
gen2drv.put(txn) start_item / finish_item
Senaryoları birleştirmek için yeni generator yazmak Hiyerarşik sequence: wr_seq.start(m_sequencer, this)
-> done + wait(...) start() bitince döner; objection test'te
txn.randomize() put'tan önce Geç rastgeleleştirme: start_item'dan sonra

En büyük kazanım birleştirilebilirliktir: main_sequence iki alt sequence'i yeniden kullanır; SystemVerilog projesinde aynı şeyi yapmak için üçüncü bir generator sınıfı yazmanız gerekirdi.

Sık Yapılan Hatalar

  • set ve get'te farklı tip parametresi: set tarafında #(int), get tarafında #(bit [31:0]) yazılırsa eşleşme olmaz ve get sessizce 0 döner. Tip birebir aynı olmalı; karmaşık tipler için typedef kullanın.
  • inst_name yolunda yazım hatası: "env_inst.agent.drv" (olmayan agent) hiçbir bileşenle eşleşmez; get başarısız olur. uvm_warning/uvm_fatal ile dönüşü kontrol etmek bu yüzden şarttır. Hata ayıklamak için +UVM_CONFIG_DB_TRACE komut satırı seçeneğini kullanın.
  • get'i run_phase'de yapmak: Çalışır, ama konfigürasyon build_phase'de okunmalıdır; aksi halde değer bağlantılar yapıldıktan sonra gelir ve yapıyı etkileyemez (örneğin is_active).
  • randomize() dönüşünü kontrol etmemek: Kısıt çakışmasında eski (sıfır) değerler sürülür, test "geçer" ama hiçbir şey doğrulanmaz. if (!req.randomize() with {...}) uvm_error(...)` kalıbını koruyun.
  • finish_item'dan sonra req'i değiştirmek: Driver aynı handle'ı tutuyor olabilir; sequence veriyi değiştirirse driver'ın sürdüğü değer de değişir. Yeni paket için yeni create.
  • Alt sequence'i start(m_sequencer) yerine start(null) ile başlatmak: Sequencer'sız sequence start_item'da hata verir.
  • Objection'ı sequence içinde kaldırmak: Çalışır ama yüzlerce sequence'te performansı düşürür ve bitiş mantığını dağıtır; test seviyesinde tutun.

Kendinizi Deneyin

  1. uvm_config_db#(int)::set(this, "env_inst.agent_inst.drv", "driver_delay", 35); satırındaki yolu "env_inst.agent_inst.*" yapın. Hâlâ çalışıyor mu? Sonra "env_inst.agent_inst.mon" yapın: driver ne basıyor, hangi gecikme kullanılıyor?
  2. set tarafında #(int) kalsın, get tarafında #(bit [31:0]) yazın. DRV_CFG mesajı hangi dalda çıktı? Neden?
  3. main_sequence'e üçüncü bir aşama ekleyin: repeat (3) ile write_sequence ve read_sequence'i dönüşümlü koşturun (yaz-oku-yaz-oku...). Driver loglarında sıralamayı doğrulayın.
  4. write_sequence'teki req.randomize() çağrısını start_item'dan önceye alın. Çalıştı mı? Şimdi driver'a şu özelliği ekleyin: item_done() öncesinde uvm_config_db#(int)::set(null, "*", "last_addr", req.addr) yazsın; sequence ise randomize() with { addr != last_addr; } yapsın. Geç rastgeleleştirme olmadan bu kısıt neden işe yaramaz?
  5. Komut satırına +UVM_CONFIG_DB_TRACE ekleyin ve her set/get için UVM'in bastığı izleme mesajlarını inceleyin. Hangi yolun hangi bileşenle eşleştiğini görebiliyor musunuz?
Hızlı Kontrol: uvm_config_db ile set yaparken hedef neden string yol olarak yazılır, nesne referansı olarak değil?

Çünkü build_phase yukarıdan aşağıya çalışır: test set yaparken env_inst.agent_inst.drv henüz yaratılmamıştır; olmayan bir nesnenin referansı verilemez. String yol, "ileride bu yolda yaratılacak bileşen bu değeri bulsun" anlamına gelir ve joker (*) karakterlerle geniş eşleşmeye izin verir.

Özet ve Beklenen Sonuç

Simülasyon çalıştırıldığında testbench aşağıdaki sırayla çalışacaktır:

  1. build_phase sırasında test, config_db'ye 35 değerini yazacak.
  2. Driver, config_db'den 35 değerini başarıyla okuduğunu loglayacak (DRV_CFG).
  3. run_phase'de test main_sequence'i başlatacak.
  4. main_sequence sırasıyla iki adet YAZMA (write_en=1), ardından iki adet OKUMA (write_en=0) işlemi üretecek.
  5. Driver bu işlemleri alıp ekrana yazdıracak (Driving Transaction...) ve her işlemde 35 zaman birimi bekleyecek.
  6. İşlemler bittikten 50 zaman birimi sonra objection düşülecek ve simülasyon başarıyla tamamlanacaktır.

4. Gün Sıkça Sorulan Sorular (FAQ)

Bu bölüm eğitim sırasında 4. Gün konuları ile ilgili olarak öğrencilerden gelebilecek soruları ve yanıtlarını içerir.

Soru 1: uvm_config_db kullanırken neden "set" işleminde hedef yolunu "env_inst.agent_inst.drv" şeklinde string (metin) olarak yazıyoruz? Sınıf referanslarını doğrudan kullansak olmaz mı?

Cevap: build_phase yukarıdan aşağıya (top-down) çalışır. Biz base_test'in build_phase'i içerisindeyken, agent_inst ve drv henüz yaratılmamış olabilir (bellekte yer kaplamazlar). Olmayan bir objenin referansını veremezsiniz. Bu yüzden UVM, "İleride bu yolda (path) yaratılacak olan bileşen bu değeri alsın" mantığıyla string tabanlı hiyerarşik bir adresleme sistemi kullanır.

Soru 2: Neden uvm_do gibi daha kısa makroları kullanmak yerine transaction yaratma, kısıtlama (randomize) ve gönderme işlemlerini uzun uzun elimizle yazıyoruz?

Cevap: Eski UVM kodlarında uvm_do makroları çok popülerdi. Ancak bu makrolar arka planda tam olarak ne olduğunu (ve hata çıktığında hangi satırda patladığını) gizlerler. Modern UVM standartlarında ve endüstrideki büyük projelerde, debug edilebilirliğinin (hata ayıklama) yüksek olması nedeniyle API tabanlı start_item / finish_item yapısı zorunlu tutulmaktadır.

Soru 3: Sequence'lar (uvm_sequence) neden uvm_component değil de uvm_object soyundan geliyor?

Cevap: uvm_component sınıfları (Env, Agent, Driver) donanım iskeleti gibidir; simülasyonun 0. anında yaratılırlar ve simülasyon bitene kadar asla yok edilmezler. Sequence'lar ise dinamiktir. Simülasyonun ortasında yaratılır, içindeki görevleri yapar ve işleri bitince bellekten silinirler. Bu kısa ömürlü ve "veri tabanlı" yapıları nedeniyle uvm_object (daha spesifik olarak uvm_sequence_item) soyundan gelirler. Component fazlarına (build_phase, connect_phase vb.) sahip değildirler.

Soru 4: Objection (raise_objection) kaldırma/indirme işlemini neden Sequence'ın kendi içinde (body taskı içinde) yapmadık da base_test içinde yaptık?

Cevap: Sequence içinde objection kullanmak mümkündür ancak kesinlikle tavsiye edilmez. Büyük bir projede binlerce transaction ve yüzlerce alt-sequence çalışabilir. Her biri için objection raise/drop yapmak performansı inanılmaz derecede düşürür. Endüstri standardı olarak simülasyonun ne zaman başlayıp ne zaman biteceğine her zaman testin (örneğin base_test) en üst run_phase'i karar vermelidir. Test objection'ı kaldırır, ana sequence'ı başlatır, sequence bitince objection'ı düşürür.

Soru 5: Transaction'ı neden start_item çağrısından ÖNCE değil de SONRA randomize ediyoruz? Önce randomize edip sonra göndersek ne kaybederiz?

Cevap: Teknik olarak start_item'dan önce randomize etmek de derlenir ve çalışır, ancak "geç rastgeleleştirme" (late randomization) endüstride bilinçli olarak tercih edilen bir tekniktir. start_item, driver get_next_item çağırana kadar akışı bloklar; yani driver'ın gerçekten o transaction'ı isteyeceği ana kadar bekler. Veriyi tam o anda üretirsek, simülasyonun en güncel durumundan (örneğin scoreboard'dan gelen geri besleme, register değerleri veya bir önceki işlemin sonucu) faydalanabiliriz. Eğer veriyi çok erken üretirsek, transaction sıraya girene kadar geçen sürede bu bilgiler bayatlayabilir ve kısıtlarımız (randomize() with) eski duruma göre çözülmüş olur. Kısacası geç randomize, en akıllı ve en güncel stimulus'u üretmenin yoludur.

Soru 6: Hiyerarşik sequence'te alt sequence'ı wr_seq.start(m_sequencer, this) ile başlatırken kullandığımız m_sequencer tam olarak nedir ve nereden gelir?

Cevap: m_sequencer, her sequence'ın içinde otomatik olarak bulunan, o sequence'ın hangi sequencer üzerinde koştuğunu gösteren yerleşik (built-in) bir tutamaçtır. Biz ana sequence'ı testte m_seq.start(env_inst.agent_inst.sqr) ile başlattığımızda, UVM m_seq'in m_sequencer alanını otomatik olarak o sequencer'a (sqr) ayarlar. Dolayısıyla ana sequence'ın body'si içindeyken m_sequencer zaten doğru sequencer'a işaret eder. Alt sequence'ları başlatırken bu değeri tekrar kullanmak, onları el yordamıyla bir sequencer referansı bulmaya zorlamadan aynı sequencer'a bağlamamızı sağlar. İkinci argüman olarak verdiğimiz this ise alt sequence'ın "parent"ını ana sequence yapar; bu parent/child ilişkisi öncelik (priority) ve ilerleme yönetiminin sağlıklı çalışması için önemlidir.