Testiniz PASS Verdi, Peki Gerçekten Bir Şey Test Etti mi? UVM'de Yeşil Yalan Söyleyen 7 Sessiz Hata

Blog · Pratik Rehber | 10 Kasım 2026

Şöyle bir sahne düşünün. Gece regression'ı bitmiş, 412 testin 412'si yeşil. Coverage raporu fena değil, kimse log açmıyor, tasarım bir sonraki aşamaya geçiyor. Haftalar sonra kartta bir hata çıkıyor ve biri "bunun testi vardı, geçiyordu" diyor. Log açılıyor: test sıfır nanosaniyede bitmiş. Tek bir işlem bile sürülmemiş. UVM_ERROR : 0, çünkü hata verecek bir şey hiç olmamış.

Doğrulamada en pahalı hata FAIL veren test değildir; o en azından kendini belli eder. Pahalı olan, hiçbir şey kanıtlamadığı hâlde PASS veren testtir. Üstelik UVM bu konuda cömerttir: kütüphanenin esnekliğini sağlayan pek çok mekanizma, yanlış kullanıldığında hata vermek yerine sessizce hiçbir şey yapmaz.

Bu yazıda bir UVM testbench'inin size yeşil yalan söylemesinin yedi yaygın yolunu, her birinin neden sessiz kaldığını ve hepsini birden yakalayan birkaç satırlık bir savunma hattını anlatıyoruz. Örnekler derslerdeki my_driver, my_transaction ve base_test adlarını kullanıyor; kod UVM 1.2 ile yazıldı.

Önce Bir Soru: PASS Ne Demek?

Çoğu ortamda PASS'in tanımı şudur: simülasyon bitti ve UVM_ERROR ile UVM_FATAL sayısı sıfır. Dikkat edin, bu bir yokluk tanımıdır. "Hata görmedim" der, "doğru davranış gördüm" demez. Aşağıdaki yedi hatanın ortak noktası da budur: hepsi hata üretecek mekanizmayı devre dışı bırakır, dolayısıyla hata sayısı sıfır kalır.

# Sessiz hata Görünen sonuç Gerçekte olan
1 Objection kaldırılmadı PASS, çok hızlı Simülasyon 0 zamanda bitti
2 Analysis port bağlanmadı PASS Scoreboard hiç veri almadı
3 config_db::get sonucu kontrol edilmedi PASS Ayar yerine varsayılan değer kullanıldı
4 create yerine new() PASS Override uygulanmadı, yanlış test koştu
5 randomize() sonucu kontrol edilmedi PASS Aynı işlem tekrar tekrar sürüldü
6 Faz metodunun adı yanlış yazıldı PASS Metot hiç çağrılmadı
7 Test erken bitti PASS Son işlemler hiç karşılaştırılmadı

1. Kaldırılmayan Objection: Sıfır Nanosaniyelik Test

Klasiklerin klasiği. run_phase yalnızca birisi objection kaldırdığı sürece yaşar. Kimse kaldırmazsa UVM "yapılacak iş yok" sonucuna varır ve fazı hemen bitirir.

task run_phase(uvm_phase phase);
  my_sequence seq = my_sequence::type_id::create("seq");
  // phase.raise_objection(this);     <-- unutuldu
  seq.start(env.agent.sqr);
  // phase.drop_objection(this);
endtask

seq.start() çağrılır, ama ilk saat kenarını bile beklemeden faz sonlanır ve çalışan tüm süreçler öldürülür. Rapor özeti tertemizdir: sıfır hata, sıfır uyarı.

Bunun sinsi bir türü de var: objection yanlış yerde kaldırılır. Örneğin sequence'in pre_body() metodunda starting_phase üzerinden objection kaldıran eski tarz kod, sequence start() ile doğrudan başlatıldığında starting_phase boş (null) olduğu için hiçbir şey yapmaz. Kod oradadır, okuyan herkes objection'ın kaldırıldığını sanar.

Belirti: Log'un sonundaki simülasyon zamanı. $finish zamanı 0 ise ya da testten teste hiç değişmiyorsa şüphelenin. +UVM_OBJECTION_TRACE kimin ne zaman kaldırıp indirdiğini gösterir. Objection mekanizmasının ayrıntısı UVM Gün 1 dersinde.

2. Bağlanmayan Analysis Port: Sağır Scoreboard

TLM'de put ve get portları bağlanmazsa elaboration sonunda hata alırsınız. Analysis port ise bilinçli olarak farklıdır: sıfır aboneye izin verir. Yayın yapan taraf dinleyen olup olmadığını bilmez, bilmek zorunda da değildir. write() çağrısı abone listesinde döner; liste boşsa döngü hiç çalışmaz.

function void connect_phase(uvm_phase phase);
  super.connect_phase(phase);
  agent.mon.ap.connect(cov.analysis_export);
  // agent.mon.ap.connect(sb.analysis_export);   <-- yeniden duzenleme sirasinda silindi
endfunction

Monitor sadakatle işlem yayınlar, coverage toplanır, scoreboard ise boş bir odada bekler. Scoreboard'un tek hata kaynağı karşılaştırma olduğundan ve hiç karşılaştırma yapılmadığından test geçer. Coverage'ın dolu görünmesi durumu daha da inandırıcı kılar.

Belirti: Scoreboard'un report_phase özeti yoksa ya da "0 karşılaştırma" diyorsa. Bu yüzden her scoreboard karşılaştırma sayısını tutmalı ve raporlamalıdır. Analysis port ve scoreboard yapısı UVM Gün 3 dersinde.

3. Kontrol Edilmeyen config_db::get: Yazım Hatasının Bedeli

uvm_config_db alan adlarını string olarak eşleştirir. Derleyici string'in içine bakamaz; "num_items" ile "num_item" arasındaki fark ancak çalışma zamanında, o da siz sorarsanız ortaya çıkar.

// Test: 500 islemlik stres testi kuruyor
uvm_config_db #(int)::set(this, "env.agent.sqr", "num_items", 500);

// Sequence: alan adinda tek harf eksik, donus degeri de atilmis
int n = 10;
void'(uvm_config_db #(int)::get(m_sequencer, "", "num_item", n));
repeat (n) begin ... end

get başarısız olur, 0 döndürür, n varsayılan değeri olan 10'da kalır. "500 işlemlik stres testi" 10 işlemle koşar ve geçer. Testin adı stres testidir, regression tablosunda stres testi olarak görünür, ama stres falan yoktur. Aynı hata yol (path) tarafında da yapılır: "env.agent.sqr" yerine "env.agnt.sqr" yazmak da aynı sonucu verir.

En tehlikeli biçimi virtual interface dışındaki ayarlarda görülür. Interface alınamazsa ilk erişimde null hatasıyla simülasyon çöker, yani kendini belli eder. Bir sayı, bir mod bayrağı ya da bir hata enjeksiyon oranı alınamazsa hiçbir şey çökmez.

Savunma: Zorunlu ayarlarda dönüş değerini her zaman kontrol edin, isteğe bağlı olanlarda en azından kullanılan değeri log'a yazın.

if (!uvm_config_db #(int)::get(m_sequencer, "", "num_items", n))
  `uvm_fatal("NOCFG", "num_items ayari bulunamadi")
`uvm_info("CFG", $sformatf("num_items = %0d", n), UVM_LOW)

+UVM_CONFIG_DB_TRACE her set ve get çağrısını yol ve alan adıyla birlikte döker; eşleşmeyen çifti bulmanın en hızlı yolu budur. Ayrıntı UVM Gün 4 dersinde.

4. create Yerine new(): Hiç Uygulanmayan Override

Hata enjeksiyon testi yazdınız. Test, normal driver yerine bozuk CRC üreten bir driver kuruyor ve DUT'un bu paketleri reddetmesini bekliyor:

class crc_error_test extends base_test;
  function void build_phase(uvm_phase phase);
    my_driver::type_id::set_type_override(my_error_driver::get_type());
    super.build_phase(phase);
  endfunction
endclass

Ama agent'ın içinde, belki yıllar önce yazılmış tek bir satır var:

drv = new("drv", this);                          // factory'ye ugramaz
// drv = my_driver::type_id::create("drv", this);   // olmasi gereken

new() doğrudan yapıcıyı çağırır; factory'ye hiç sorulmadığı için override tablosuna da bakılmaz. Override kaydı hata vermez, uyarı vermez, yalnızca kullanılmadan kalır. crc_error_test normal driver ile, tek bir bozuk paket göndermeden koşar. Scoreboard düzgün paketleri görür, mutlu olur. Regression tablosunda "CRC hata testi: PASS" yazar.

Bu, yedi hatanın belki de en zararlısı, çünkü test adı ile testin yaptığı iş arasındaki bağı koparır. Hata işleme mantığınızın doğrulandığını sanırsınız; oysa o kod satırları hiç çalışmamıştır.

Savunma: Override kullanan her test, beklediği tipin gerçekten yaratıldığını kendi doğrulamalı. end_of_elaboration_phase içinde bir $cast yeterli:

function void end_of_elaboration_phase(uvm_phase phase);
  my_error_driver ed;
  if (!$cast(ed, env.agent.drv))
    `uvm_fatal("OVRD", "Override uygulanmadi: drv hala my_driver")
endfunction

Ayrıca kod incelemesinde uvm_component ve uvm_object türevleri için new( araması yapmak ucuz ve etkili bir alışkanlıktır. Factory'nin iç işleyişi UVM Gün 5 dersinde.

5. Kontrol Edilmeyen randomize(): Aynı Paket, Bin Kez

Constraint'ler çeliştiğinde randomize() 0 döndürür ve rastgele değişkenlere dokunmaz; hepsi önceki değerlerinde kalır.

repeat (1000) begin
  req = my_transaction::type_id::create("req");
  start_item(req);
  void'(req.randomize() with { addr inside {[32'h1000:32'h1FFF]}; });
  finish_item(req);
end

Sınıfın içinde addr < 32'h0800 gibi bir constraint varsa çözücü her seferinde başarısız olur. Sonuç: 1000 adet, alanları varsayılan değerinde (büyük olasılıkla sıfır) duran, birbirinin aynısı işlem. DUT bunları doğru işler, scoreboard 1000 başarılı karşılaştırma sayar. Karşılaştırma sayacınız bile sizi kurtarmaz; bin karşılaştırma yapılmıştır, hepsi de aynı karşılaştırmadır.

Simülatörler genellikle bir uyarı basar, ama bu uyarı UVM'in rapor sayaçlarına girmez. `uvm_do_with makrosu da durumu çok değiştirmez: UVM 1.2'de başarısızlık UVM_ERROR değil UVM_WARNING olarak raporlanır ve uyarılar çoğu ortamda testi düşürmez.

Savunma: Tek satır.

if (!req.randomize() with { addr inside {[32'h1000:32'h1FFF]}; })
  `uvm_fatal("RAND", "Randomization basarisiz")

Burada sizi asıl koruyan ikinci hat functional coverage'dır: addr için tanımlı 16 bin'den yalnızca biri doluysa bir şeyler yanlıştır. Coverage "ne kadar test ettim" sorusundan önce "sandığım şeyi test ettim mi" sorusuna cevap verir. Konunun temeli randomize() dersinde.

6. Yanlış Yazılan Faz Adı: Hiç Çağrılmayan Metot

UVM fazları uvm_component içindeki boş virtual metotlardır. Siz aynı adla bir metot yazarak onları ezersiniz. Adı tutturamazsanız ortada ezilen bir şey olmaz; yazdığınız metot, kimsenin çağırmadığı sıradan bir metot olarak derlenir.

function void conect_phase(uvm_phase phase);     // "connect" degil
  drv.seq_item_port.connect(sqr.seq_item_export);
endfunction

task run_phse(uvm_phase phase);                  // "run_phase" degil
  forever begin ... end
endtask

Derleyici şikâyet etmez, çünkü bu geçerli SystemVerilog'dur. İlk örnek en azından ilk get_next_item çağrısında hata verir. İkincisi vermez: monitor'ün run_phase'i yanlış yazılmışsa monitor hiçbir şey gözlemez, hiçbir şey yayınlamaz ve 2 numaralı hatadaki sağır scoreboard tablosu yeniden kurulur. Aynı durum check_phase ya da report_phase için daha da sessizdir: test sonu kontrolleriniz yazılmıştır ama asla çalışmaz.

Dil tarafında bu soruna çare olarak IEEE 1800-2023, bir metodun üst sınıftaki bir metodu ezdiğini açıkça belirtmeye yarayan :extends belirtecini getirdi; ancak araç desteği henüz sınırlı ve UVM kod tabanlarında yaygın değil.

Savunma: Bugün için en pratik yol, sonuçtan yakalamak: her bileşen yaptığı işi saysın (sürülen, gözlenen, karşılaştırılan işlem sayısı) ve sıfır kalan sayaç hata olsun. Bir sonraki bölümdeki kontrol tam olarak bunu yapıyor.

7. Erken Biten Test: Karşılaştırılmayan Son İşlemler

Test, sequence biter bitmez objection'ı indirir. Oysa son işlemler hâlâ DUT'un içindedir: boru hattında (pipeline) ilerliyor, FIFO'da bekliyor ya da çıkış arayüzünde el sıkışmayı bekliyordur. Faz biter, bu işlemlerin çıktısı hiç gözlenmez.

phase.raise_objection(this);
seq.start(env.agent.sqr);
phase.drop_objection(this);      // son paket hala DUT'un icinde

Scoreboard'un beklenen işlem kuyruğunda öğeler kalır, ama kimse kuyruğa bakmazsa bu bir hata değildir. İşin kötüsü, hatalar tam da burada saklanmayı sever: son paketin kaybolması, FIFO'nun son elemanının çıkmaması, kapanış sırasındaki durum makinesi hataları. Yani testin kör noktası, tasarımın en kırılgan olduğu anla çakışır.

Savunma: İki parçalı. Önce DUT'a boşalması için süre tanıyın:

phase.raise_objection(this);
phase.phase_done.set_drain_time(this, 200);
seq.start(env.agent.sqr);
phase.drop_objection(this);

Sonra, asıl önemlisi, scoreboard kuyruğunun boşaldığını test sonunda kontrol edin. Sabit bir bekleme süresi tahmindir; kuyruk kontrolü ise kanıttır.

Savunma Hattı: PASS'i Hak Edilen Bir Şeye Çevirmek

Yedi hatanın altısı aynı yere çıkıyor: beklenen iş yapılmadı ve kimse fark etmedi. O hâlde çözüm de ortak: PASS'i hatanın yokluğuyla değil, işin varlığıyla tanımlayın. Scoreboard'a eklenecek birkaç satır tablonun büyük kısmını kapatır:

class my_scoreboard extends uvm_scoreboard;
  my_transaction exp_q[$];
  int unsigned   n_compared;
  int unsigned   min_compares = 1;    // test, config_db ile yukseltebilir
  ...
  function void check_phase(uvm_phase phase);
    super.check_phase(phase);
    if (n_compared < min_compares)
      `uvm_error("SB_FEW", $sformatf("Beklenen en az %0d karsilastirma, yapilan %0d",
                                     min_compares, n_compared))
    if (exp_q.size() != 0)
      `uvm_error("SB_PEND", $sformatf("%0d beklenen islem karsiliksiz kaldi", exp_q.size()))
  endfunction

  function void report_phase(uvm_phase phase);
    `uvm_info("SB", $sformatf("Toplam karsilastirma: %0d", n_compared), UVM_NONE)
  endfunction
endclass

Bu on satır neyi yakalar?

Hata Yakalayan kontrol
1. Objection yok n_compared == 0
2. Port bağlı değil n_compared == 0
3. Yanlış config (10 işlem, 500 beklenirken) Test min_compares değerini 500 yaparsa SB_FEW
6. Faz adı yanlış (monitor/driver çalışmıyor) n_compared == 0
7. Erken bitiş exp_q.size() != 0

Kalan iki hata (override ve randomize) işin miktarını değil niteliğini bozar; onlar için yukarıdaki $cast kontrolü, randomize() dönüş değeri kontrolü ve functional coverage gerekir.

Son bir halka daha var: PASS/FAIL kararını kimin verdiği. Karar tek bir yerde, rapor sunucusunun sayaçlarından verilmeli ve regression betiği yalnızca o satıra bakmalı:

function void base_test::report_phase(uvm_phase phase);
  uvm_report_server svr = uvm_report_server::get_server();
  if (svr.get_severity_count(UVM_FATAL) + svr.get_severity_count(UVM_ERROR) == 0)
    `uvm_info("RESULT", "*** TEST PASSED ***", UVM_NONE)
  else
    `uvm_info("RESULT", "*** TEST FAILED ***", UVM_NONE)
endfunction

Burada da iki tuzak var. Birincisi: action bloğu olmayan bir SystemVerilog assertion'ı başarısız olduğunda simülatörün kendi hata mesajını basar; bu mesaj UVM sayaçlarına girmez. Assertion'larınızın else kolunda `uvm_error çağırın ya da betiğiniz simülatörün assertion özetine de baksın (assertion dersi). İkincisi: betik "PASSED" satırını ararken satırın yokluğunu da FAIL saymalı. Çöken, zaman aşımına uğrayan ya da lisans alamayan bir simülasyon hiçbir şey yazmaz; "FAILED yoksa geçmiştir" mantığı bunları yeşile boyar.

Kendi Testbench'inizi Sınayın: Beş Dakikalık Deney

Bunların sizde olup olmadığını öğrenmenin kolay bir yolu var: ortamınızı bilerek bozun ve ne olduğuna bakın. Mutation testing fikrinin elle yapılan, küçük bir sürümü.

  1. Test'teki raise_objection satırını yorum yapın. Test FAIL veriyor mu?
  2. Scoreboard'un connect satırını yorum yapın. FAIL veriyor mu?
  3. Bir config_db::set çağrısındaki alan adına bir harf ekleyin. Fark eden oluyor mu?
  4. Scoreboard'un karşılaştırma koşulunu if (0) yapın. FAIL veriyor mu?
  5. DUT'ta bir satırı bilerek bozun (bir + işaretini - yapın). Kaç test düşüyor?

Beşinci madde en öğreticisidir. Bozuk bir tasarıma karşı regression'ınız hâlâ tamamen yeşilse, o yeşil size tasarım hakkında bir şey söylemiyor demektir. Bu deneylerden herhangi biri PASS ile sonuçlanıyorsa moraliniz bozulmasın; bunu bugün, masanızda öğrenmek kartta öğrenmekten çok daha ucuzdur.

Sonuç

UVM'in sessizliği bir kusur değil, tasarım tercihidir: analysis port'un abonesiz çalışabilmesi, config ayarlarının isteğe bağlı olabilmesi, fazların ezilmeden bırakılabilmesi kütüphaneyi yeniden kullanılabilir yapan şeylerdir. Bedeli, "hiçbir şey olmadı" ile "her şey yolunda" durumlarının dışarıdan aynı görünmesidir.

Bu ikisini ayırmak testbench'i yazanın işidir ve kuralı basittir: her test, yapmayı vaat ettiği işi yaptığını kendisi kanıtlamalı. Kaç işlem sürdü, kaçını karşılaştırdı, kuyrukta ne kaldı, beklediği tip gerçekten yaratıldı mı. Yeşil bir regression tablosu ancak bu sorular test sonunda otomatik olarak cevaplanıyorsa bir anlam taşır.

Bu Sitedeki Derslerle İlişkisi

Buradaki her hata, UVM serisinde anlatılan bir mekanizmanın ters yüzüdür: objection ve fazlar Gün 1, analysis port ve scoreboard Gün 3, config_db Gün 4, factory ve override Gün 5 derslerinde. Mekanizmaların neden böyle tasarlandığını merak ediyorsanız UVM 1.2'nin mimarisi yazısı kaynak kod üzerinden anlatıyor. Sıfırdan bir testbench iskeleti kurmak isterseniz UVM Generator aracını deneyebilirsiniz.