EDA Playground'da Dene

ALU Design Under Test(DUT)

ALU Tasarım Kodları | Bitirme projesi

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.