Assignment Chef icon Assignment Chef
All German tutorials

Programming lesson

ALU und Steuerwerk in Verilog: Ein praxisnaher Leitfaden für CDA 4205L Lab #7

Erfahren Sie Schritt für Schritt, wie Sie eine ALU und ein Steuerwerk (Control Unit) in Verilog für das Lab #7 von CDA 4205L entwerfen. Inklusive Testbench und typischen Fehlerquellen.

ALU Verilog Steuerwerk Control Unit CDA 4205L Lab 7 Verilog ALU Design RISC-V Steuerwerk Testbench Verilog EDA Playground Icarus Verilog ALU Operationen Control Unit Decodierung Verilog signed Arithmetik CPU Design Studium RISC-V ALU Tutorial Verilog case statement Hardwarebeschreibungssprache Prozessorarchitektur

Einführung in ALU und Steuerwerk

Die Arithmetic Logic Unit (ALU) und das Steuerwerk (Control Unit) sind das Herz jeder CPU. In diesem Tutorial lernen Sie, wie Sie beide Komponenten in Verilog für das CDA 4205L Lab #7 modellieren. Wir verwenden dabei den RV32IM-Befehlssatz und simulieren mit Icarus Verilog auf EDA Playground. Aktuell (Juni 2026) sind solche Grundlagen auch für das Verständnis moderner KI-Beschleuniger und RISC-V-Prozessoren in Smartphones relevant.

Grundlagen der ALU

Die ALU führt arithmetische und logische Operationen auf zwei 32-Bit-Operanden (A und B) aus. Die Auswahl der Operation erfolgt über einen 4-Bit-Steuereingang. Typische Operationen sind Addition, Subtraktion, Multiplikation, Division, Bit-Shifts und Vergleiche. In Verilog realisieren wir dies mit einer case-Anweisung innerhalb eines always-Blocks.

Moduldefinition und Timescale

`timescale 1ns/1ns
module alu (
    input  signed [31:0] A, B,
    input  [3:0] operation,
    output reg signed [31:0] ALUResult
);

Die timescale-Direktive definiert die Zeiteinheit für die Simulation. Alle Eingänge und Ausgänge sind als signed deklariert, um korrekte vorzeichenbehaftete Arithmetik zu gewährleisten.

Implementierung der ALU-Operationen

always @(*) begin
    case (operation)
        4'b0010: ALUResult = A + B;  // add
        4'b0110: ALUResult = A - B;  // sub
        4'b0111: ALUResult = A * B;  // mul
        4'b1111: ALUResult = A / B;  // div
        4'b0100: ALUResult = A << B; // sll
        4'b0101: ALUResult = A >> B; // srl
        4'b1110: ALUResult = A >>> B;// sra
        4'b0000: ALUResult = A & B;  // and
        4'b0001: ALUResult = A | B;  // or
        4'b0011: ALUResult = A ^ B;  // xor
        4'b1000: ALUResult = (A == B) ? 32'd1 : 32'd0; // eq
        4'b1001: ALUResult = (A != B) ? 32'd1 : 32'd0; // neq
        4'b1010: ALUResult = (A < B) ? 32'd1 : 32'd0;  // lt
        4'b1011: ALUResult = (A >= B) ? 32'd1 : 32'd0; // geq
        default: ALUResult = 0;
    endcase
end

Beachten Sie, dass Division durch Null vermieden werden sollte. In der Praxis würde man eine Fehlerbehandlung einbauen, aber hier setzen wir einfach das Ergebnis auf 0.

Testbench für die ALU

Eine Testbench ist ein eigenes Modul ohne Ein-/Ausgänge, das die ALU mit Testdaten versorgt und die Ergebnisse überwacht. Wir deklarieren reg für Eingänge und wire für Ausgänge.

module alu_testbench;
    reg signed [31:0] A, B;
    reg [3:0] operation;
    wire signed [31:0] ALUResult;

    alu uut (
        .A(A),
        .B(B),
        .operation(operation),
        .ALUResult(ALUResult)
    );

    initial begin
        $monitor($time, " A=%d, B=%d, op=%b, result=%d", A, B, operation, ALUResult);
        A = 32'd10; B = 32'd3;
        #5 operation = 4'b0010; // add
        #5 operation = 4'b0110; // sub
        #5 operation = 4'b0111; // mul
        #5 operation = 4'b1111; // div
        #5 operation = 4'b0100; // sll
        #5 operation = 4'b0101; // srl
        #5 operation = 4'b1110; // sra
        #5 operation = 4'b0000; // and
        #5 operation = 4'b0001; // or
        #5 operation = 4'b0011; // xor
        #5 operation = 4'b1000; // eq
        #5 operation = 4'b1001; // neq
        #5 operation = 4'b1010; // lt
        #5 operation = 4'b1011; // geq
        #10 $finish;
    end
endmodule

Mit $monitor werden alle Änderungen automatisch ausgegeben. Die Verzögerungen (#5) simulieren Taktzyklen. Nach dem Test sollten Sie auch negative Zahlen testen, um die signed-Arithmetik zu prüfen.

Steuerwerk (Control Unit) in Verilog

Das Steuerwerk decodiert den Opcode der RISC-V-Instruktion und setzt die Steuersignale. Für dieses Lab gibt es 10 Steuerleitungen: ALUSrc, MemToReg, RegWrite, MemRead, MemWrite, Branch, Jump, Jalr und ALUOp (2 Bit).

Moduldefinition

module control_unit (
    input [6:0] opcode,
    output reg ALUSrc,
    output reg MemToReg,
    output reg RegWrite,
    output reg MemRead,
    output reg MemWrite,
    output reg Branch,
    output reg Jump,
    output reg Jalr,
    output reg [1:0] ALUOp
);

Decodierung der Opcodes

Wir unterscheiden die Hauptinstruktionsklassen von RV32I: R-Typ, I-Typ (lw, addi, etc.), S-Typ (sw), B-Typ (beq, bne), J-Typ (jal) und I-Typ für jalr. Die Zuordnung der ALUOp-Bits erfolgt gemäß der Lab-Vorgabe.

always @(*) begin
    // Standardwerte (alle 0)
    {ALUSrc, MemToReg, RegWrite, MemRead, MemWrite, Branch, Jump, Jalr} = 0;
    ALUOp = 0;
    case (opcode)
        7'b0110011: begin // R-Typ (add, sub, etc.)
            RegWrite = 1;
            ALUOp = 2'b10;
        end
        7'b0000011: begin // I-Typ lw
            ALUSrc = 1;
            MemToReg = 1;
            RegWrite = 1;
            MemRead = 1;
            ALUOp = 2'b00;
        end
        7'b0010011: begin // I-Typ addi, etc.
            ALUSrc = 1;
            RegWrite = 1;
            ALUOp = 2'b10;
        end
        7'b0100011: begin // S-Typ sw
            ALUSrc = 1;
            MemWrite = 1;
            ALUOp = 2'b00;
        end
        7'b1100011: begin // B-Typ beq, bne
            Branch = 1;
            ALUOp = 2'b01;
        end
        7'b1101111: begin // J-Typ jal
            Jump = 1;
            RegWrite = 1;
            ALUOp = 2'b00;
        end
        7'b1100111: begin // I-Typ jalr
            Jalr = 1;
            RegWrite = 1;
            ALUOp = 2'b00;
        end
        default: begin
            // alle Signale bleiben 0
        end
    endcase
end

Testbench für das Steuerwerk

Ähnlich wie bei der ALU testen wir alle relevanten Opcodes.

module control_unit_testbench;
    reg [6:0] opcode;
    wire ALUSrc, MemToReg, RegWrite, MemRead, MemWrite, Branch, Jump, Jalr;
    wire [1:0] ALUOp;

    control_unit uut (
        .opcode(opcode),
        .ALUSrc(ALUSrc),
        .MemToReg(MemToReg),
        .RegWrite(RegWrite),
        .MemRead(MemRead),
        .MemWrite(MemWrite),
        .Branch(Branch),
        .Jump(Jump),
        .Jalr(Jalr),
        .ALUOp(ALUOp)
    );

    initial begin
        $monitor($time, " opcode=%b, ALUSrc=%b, MemToReg=%b, RegWrite=%b, MemRead=%b, MemWrite=%b, Branch=%b, Jump=%b, Jalr=%b, ALUOp=%b",
                 opcode, ALUSrc, MemToReg, RegWrite, MemRead, MemWrite, Branch, Jump, Jalr, ALUOp);
        #5 opcode = 7'b0110011; // R-Typ
        #5 opcode = 7'b0000011; // lw
        #5 opcode = 7'b0010011; // addi
        #5 opcode = 7'b0100011; // sw
        #5 opcode = 7'b1100011; // beq
        #5 opcode = 7'b1101111; // jal
        #5 opcode = 7'b1100111; // jalr
        #10 $finish;
    end
endmodule

Häufige Fehler und Tipps

  • Vergessen von `timescale: Ohne diese Direktive funktionieren Verzögerungen nicht korrekt.
  • Falsche Port-Reihenfolge: Bei der Instanziierung muss die Reihenfolge der Ports mit dem Modul übereinstimmen oder man verwendet named ports (wie im Beispiel).
  • Vergessen von signed: Ohne signed werden Vergleiche wie A < B unsigned interpretiert.
  • Division durch Null: Vermeiden Sie dies oder behandeln Sie es im default-Zweig.
  • Testbench ohne $monitor: Ohne diese Anweisung sehen Sie keine Ausgaben.

Zusammenfassung

Sie haben nun eine voll funktionsfähige ALU und ein Steuerwerk in Verilog entworfen und getestet. Diese Komponenten sind essenziell für das Verständnis von Prozessoren und bilden die Grundlage für komplexere Designs wie Pipelining oder Multicore-Systeme. Mit den heutigen Trends im Bereich RISC-V und Open-Source-Hardware sind diese Kenntnisse besonders wertvoll. Viel Erfolg bei Ihrem Lab #7!