EDA Playground'da Dene

ALU Tasarımı (DUT)

Gün 6: Bitirme Projesi - Bölüm 1 | Doğrulanacak tasarım: 8-bit pipeline'lı ALU

Bitirme Projesine Genel Bakış

Gün 6 ve Gün 7 boyunca, Gün 1–5'te öğrendiğiniz her parçayı kullanarak tam bir sınıf tabanlı testbench inşa edeceksiniz. Derslerin sırası, bir doğrulama ortamının gerçek hayatta kurulma sırasını izler:

Gün 6                                     Gün 7
─────────────────────────────────────     ──────────────────────────────────────────
36-1 ALU (DUT)          → neyi test edeceğiz?
36   Interface          → DUT'a nasıl bağlanacağız?
37   Transaction        → veriyi nasıl soyutlayacağız?
38   Generator          → uyarıcıyı kim üretecek?
39   Driver             → pinlere kim sürecek?
40   Ara Test           → gen + drv çalışıyor mu?   41 Monitor     → DUT ne yaptı?
                                                     42 Scoreboard  → doğru mu yaptı?
                                                     43 Environment → hepsini kim bağlar?
                                                     44 Test        → hangi senaryo?
                                                     45 Coverage    → yeterince denedik mi?
                                                     46 Final Top   → hepsini çalıştır

Her ders tek bir dosyaya karşılık gelir ve final testbench bu dosyaları `include ile birleştirir. Bu dersin EDA Playground bağlantısı tüm projeyi içerir; ilgili dersin dosyasını orada bulup inceleyebilirsiniz.

Bu bölümde, tüm doğrulama projemizin merkezinde yer alan DUT (Design Under Test / Test Edilen Tasarım) olan 8-bit ALU modülünü inceliyoruz. Testbench mimarisinde DUT, doğruluğunu kanıtlamaya çalıştığımız asıl donanımdır; driver onu uyarır, monitor onu gözler ve scoreboard onun çıkışlarını referans modelle karşılaştırır.

DUT Nedir ve Mimarideki Yeri

DUT (Design Under Test), doğrulama ortamının kalbidir. Testbench'in tüm diğer bileşenleri (interface, driver, monitor, scoreboard...) yalnızca bu modülün doğru çalışıp çalışmadığını anlamak için vardır.

Bu ALU şu temel sözleşmeyi (contract) sunar:

  • Girişler: clk, rst_n, in_valid, operand_a, operand_b, opcode
  • Çıkışlar: out_valid, result, flags
  • DUT, dış dünya ile yalnızca bu portlar üzerinden konuşur. Testbench içindeki alu_if arayüzü tam olarak bu portlara bağlanır.

Senkron Tasarım ve Pipeline Mantığı

ALU iki ayrı bloktan oluşur:

  • Kombinasyonel blok (always_comb): Seçilen opcode'a göre aritmetik/mantıksal işlemi yapar ve sonucu next_result ile bayrakları next_flags ara sinyallerinde üretir.
  • Sıralı blok (always_ff): Saat kenarında bu ara sonuçları çıkış register'larına (result, flags, out_valid) yazar. Bu yapı bir pipeline (boru hattı) oluşturur: bir girişin sonucu bir saat çevrimi gecikmeyle çıkışta görünür.

Bu gecikme, testbench tarafında çok önemlidir: monitor, çıkışın hangi saat kenarında geçerli olacağını (out_valid) bilmek zorundadır.

Opcode ve Bayrak Sözleşmesi

Tasarımdaki localparam opcode tanımları (OP_ADD, OP_SUB, ...), ALU_Transaction sınıfındaki opcode_e enum değerleriyle birebir aynı olacak şekilde seçilmiştir. Bu uyum, transaction'ı doğrudan DUT pinlerine sürebilmemizi sağlar. Benzer şekilde bayrak bitleri (FLAG_Z, FLAG_N, FLAG_C, FLAG_P) sabit pozisyonlarda tanımlanmıştır.

Kaynak Kod

module alu (
    input  logic        clk,
    input  logic        rst_n,
    input  logic        in_valid,
    input  logic [7:0]  operand_a,
    input  logic [7:0]  operand_b,
    input  logic [2:0]  opcode,
    output logic        out_valid,
    output logic [15:0] result,
    output logic [3:0]  flags
);

    // Internal signals for combinational logic outputs
    logic [15:0] next_result;
    logic [3:0]  next_flags;

    // Opcode definitions perfectly matched with Testbench ALU_Transaction enum
    localparam bit [2:0] 
        OP_ADD = 3'b000,
        OP_SUB = 3'b001,
        OP_AND = 3'b010,
        OP_OR  = 3'b011,
        OP_XOR = 3'b100,
        OP_NOT = 3'b101,
        OP_SHL = 3'b110,
        OP_SHR = 3'b111;

    // Flag bit positions
    localparam int FLAG_Z = 0; // Zero
    localparam int FLAG_N = 1; // Negative
    localparam int FLAG_C = 2; // Carry (for 8-bit operations)
    localparam int FLAG_P = 3; // Parity

    // =========================================================================
    // Combinational Block: Arithmetic & Logic Operations
    // =========================================================================
    always_comb begin
        // Default initializations to prevent latches
        next_result = 16'd0;
        next_flags  = 4'd0;

        // Perform the operation based on the synced opcode
        case (opcode)
            OP_ADD: next_result = operand_a + operand_b;
            OP_SUB: next_result = operand_a - operand_b;
            OP_AND: next_result = {8'h00, operand_a & operand_b};
            OP_OR:  next_result = {8'h00, operand_a | operand_b};
            OP_XOR: next_result = {8'h00, operand_a ^ operand_b};
            OP_NOT: next_result = {8'h00, ~operand_a};
            OP_SHL: next_result = {8'h00, operand_a} << operand_b[2:0];
            OP_SHR: next_result = {8'h00, operand_a} >> operand_b[2:0];
            default: next_result = 16'd0;
        endcase

        // Calculate status flags
        next_flags[FLAG_Z] = (next_result == 16'd0);
        next_flags[FLAG_N] = next_result[15]; 
        
        // Carry flag makes sense mainly for ADD/SUB out of the 8th bit
        if (opcode == OP_ADD || opcode == OP_SUB) begin
            next_flags[FLAG_C] = next_result[8];
        end else begin
            next_flags[FLAG_C] = 1'b0;
        end
        
        // Parity flag (XOR reduction of the result)
        next_flags[FLAG_P] = ^next_result;
    end

    // =========================================================================
    // Sequential Block: Output Registers (Pipelining)
    // =========================================================================
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            // Reset state
            out_valid <= 1'b0;
            result    <= 16'd0;
            flags     <= 4'd0;
        end else begin
            // Pipeline the valid signal
            out_valid <= in_valid;
            
            // Only update outputs when input data is valid
            if (in_valid) begin
                result <= next_result;
                flags  <= next_flags;
            end
        end
    end

endmodule

Kodun Açıklaması

  • Modül arayüzü: module alu (...) 8-bit operand_a/operand_b girişlerini, 3-bit opcode'u ve in_valid el sıkışma sinyalini alır; 16-bit result, 4-bit flags ve out_valid üretir. Sonucun 16-bit olması toplama/çarpma benzeri işlemlerde taşmayı temsil edebilmek içindir.
  • Opcode sabitleri: localparam bit [2:0] OP_ADD = 3'b000 ... OP_SHR = 3'b111 ile 8 işlem kodlanır. Bu kodlamanın transaction enum'u ile aynı olması, doğrulamanın temel taşıdır.
  • Bayrak pozisyonları: FLAG_Z (sıfır), FLAG_N (negatif), FLAG_C (carry), FLAG_P (parity) bayraklarının bit konumlarını sabitler.
  • always_comb bloğu: Latch oluşmasını engellemek için önce next_result ve next_flags varsayılan olarak sıfırlanır. Ardından case (opcode) ile ilgili işlem yapılır. Mantıksal işlemlerde (OP_AND, OP_OR, OP_XOR, OP_NOT) 8-bit sonuç {8'h00, ...} ile 16-bit'e genişletilir; kaydırma işlemleri (OP_SHL, OP_SHR) operand_b[2:0] kadar kaydırır.
  • Bayrak hesabı: FLAG_Z sonucun sıfır olup olmadığını, FLAG_N en üst bit next_result[15]'i izler. FLAG_C yalnızca OP_ADD/OP_SUB için next_result[8]'den alınır, diğer işlemlerde 0'dır. FLAG_P, ^next_result ile sonucun parite (XOR indirgeme) değerini verir.
  • always_ff bloğu: Asenkron aktif-düşük reset (!rst_n) durumunda çıkışlar temizlenir. Normal çalışmada out_valid <= in_valid ile geçerli sinyali bir çevrim geciktirilerek pipeline'a sokulur; çıkış register'ları yalnızca in_valid yüksekken güncellenir.

Önemli Noktalar

  • always_comb içinde varsayılan atama yapmak, istenmeyen latch'lerin oluşmasını engeller; sentezlenebilir ve simülasyonda tutarlı tasarımın temel kuralıdır.
  • Çıkış register'ları bir saat çevrimi gecikme getirir (out_valid <= in_valid). Driver ve monitor bu gecikmeyi dikkate alacak şekilde tasarlanmalıdır; aksi halde yanlış zamanda örnekleme yapılır.
  • Carry bayrağı yalnızca aritmetik işlemlerde anlamlıdır; mantıksal işlemlerde bilinçli olarak 0 yapılmıştır. Referans model (scoreboard) bu davranışı birebir taklit etmelidir.
  • DUT'taki opcode kodlaması ile transaction enum'u senkron tutulmalıdır; biri değişirse diğeri de değişmeli, yoksa tüm karşılaştırmalar bozulur.
  • Reset asenkron olarak negedge rst_n ile tetiklenir; testbench reset sırasında en az birkaç saat çevrimi rst_n'i düşük tutmalıdır.
  • result yalnızca in_valid yüksekken güncellenir, ama out_valid her çevrim in_valid'i izler. Yani in_valid düştüğünde result son geçerli değeri tutar; monitor bu değeri yalnızca out_valid=1 iken okumalıdır, aksi halde eski sonucu yeni işlem sanır.
  • Bayrak sözleşmesi tam olarak şudur ve scoreboard'daki referans model bunu birebir izlemelidir:
Bit Bayrak Tanım Hangi işlemlerde anlamlı?
0 Z (Zero) result == 0 Hepsi
1 N (Negative) result[15] Pratikte yalnızca SUB (borrow ile 16-bit sonuç negatif görünür)
2 C (Carry) result[8] (8-bit işlemde 9. bit) Yalnızca ADD/SUB; diğerlerinde 0
3 P (Parity) ^result (tek sayıda 1 → 1) Hepsi

Bu DUT'ta Bilinçli Bırakılan "İlginç" Noktalar

İyi bir doğrulama mühendisi, tasarımı okurken "burası bir hata üretebilir" dediği yerleri not alır. Bu ALU'da dikkat çeken noktalar:

  • SUB ve işaret: Operandlar 8-bit işaretsiz, sonuç 16-bit. 0 - 1 işlemi 16'hFFFF üretir; N bayrağı 1, C (borrow) bayrağı 1, Z bayrağı 0 olur. Referans model bu genişlemeyi aynı biçimde yapmalıdır.
  • Kaydırma miktarı: Yalnızca operand_b[2:0] kullanılır; operand_b = 8 ile operand_b = 0 aynı sonucu verir. Transaction sınıfındaki c_shift_range kısıtı bu yüzden operand_b'yi 0–7'ye sınırlar; ama bir hata avcısı olarak 8 ve üstünü de denemek isteyebilirsiniz.
  • SHL taşması: {8'h00, 8'hFF} << 7 sonucu 16'h7F80'dir; 8-bit'e sığmaz ama 16-bit sonuçta kaybolmaz. cp_result'taki medium bin'i bu durumu yakalar.
  • Carry'nin mantıksal işlemlerde sıfırlanması: Kombinasyonel blokta açıkça yazılmıştır. Bunu unutan bir referans model, AND sonucunda result[8] zaten 0 olduğu için tesadüfen doğru çalışır; ama sözleşmeyi bilinçli yazmak her zaman daha güvenlidir.

Kendinizi Deneyin

  1. Kâğıt üzerinde hesaplayın: operand_a = 8'h80, operand_b = 8'h80, opcode = OP_ADD için result ve dört bayrak ne olur? (result = 16'h0100, Z=0, N=0, C=1, P=1.) Aynı işlemi OP_SUB için yapın.
  2. Tasarıma bilinçli bir hata enjekte edin: OP_SUB satırını operand_b - operand_a yapın. Gün 7'de scoreboard'un bunu yakaladığını göreceksiniz; şimdilik hangi test girdilerinin bu hatayı ortaya çıkarmayacağını düşünün (ipucu: a == b).
  3. always_ff içindeki if (in_valid) koşulunu kaldırın. result artık ne zaman değişiyor? Monitor'ün out_valid filtresi bu değişiklikten etkilenir mi?
Hızlı Kontrol: in_valid düştükten sonra result çıkışında ne görürsünüz?

Son geçerli işlemin sonucu. result yalnızca in_valid=1 iken güncellenir; out_valid ise in_valid'i bir çevrim gecikmeyle izler ve 0 olur. Monitor bu yüzden result'ı yalnızca out_valid=1 iken örneklemelidir.