Assignment Chef icon Assignment Chef
All German tutorials

Programming lesson

Programmierbarer BCD-Zähler in Verilog: Schritt-für-Schritt-Tutorial für FPGA-Synthese (CDA4203L)

Lerne, wie du einen programmierbaren 0-99 BCD-Zähler in Verilog entwirfst, simulierst und auf einem FPGA-Board synthetisierst. Inklusive Testbench und Timing-Analyse.

programmierbarer BCD-Zähler Verilog FPGA Tutorial CDA4203L Lab 3 Binär-zu-BCD-Konverter FPGA Synthese Anvyl Board Verilog Testbench sequentielles Hardware-Design Zähler mit max_count FPGA Programmierung BCD Ausgabe eSports Punktzähler Embedded Systems Verilog FPGA für Anfänger Digitaltechnik Übung Hardware-Beschreibungssprache

Einführung: Warum ein programmierbarer BCD-Zähler?

Im Lab 3 des Kurses CDA4203L geht es um fortgeschrittenes sequentielles Hardware-Design mit Verilog und FPGA-Synthese. Ein programmierbarer BCD-Zähler (Binary-Coded Decimal) ist ein klassisches Beispiel, das die Konzepte von Zustandsautomaten, Zählern und Binär-zu-BCD-Konvertierung vereint. Stell dir vor, du entwickelst eine Echtzeitanzeige für eine Gaming-Plattform: Der Zähler könnte die Punktzahl in einem eSports-Turnier darstellen, die von 0 bis 99 läuft und bei einem festgelegten Maximum stoppt – genau wie der max_count-Eingang in dieser Aufgabe.

Im Mai 2026 sind FPGAs in vielen Bereichen relevant: von KI-Beschleunigern bis zu Embedded Systems in Smart-Home-Geräten. Dieses Tutorial führt dich durch die Erstellung eines solchen Zählers, basierend auf der Aufgabenstellung von CDA4203L. Du lernst, wie du vorgegebene Module (Binary_bcd.v, Counter.v) nutzt und die fehlenden Module (Prog_counter.v, Final_bcd_counter.v) selbst schreibst.

Überblick über die Module

Das Top-Level-Design besteht aus vier Hauptmodulen:

  • Counter.v – Ein einfacher 7-Bit-Binärzähler mit Enable und synchronem Reset.
  • Prog_counter.v – Ein programmierbarer Zähler, der bei Erreichen eines Maximalwerts stoppt.
  • Binary_bcd.v – Konvertiert den binären Zählerstand in zwei BCD-Ziffern (vollständig vorgegeben).
  • Final_bcd_counter.v – Die Top-Level-Instanziierung, die alles verbindet.

Die Eingänge sind: run (Schalter SW7), max_count (7 Bit, Schalter SW6–SW0) und der Takt. Die Ausgänge sind digit_2 (Zehnerstelle) und digit_1 (Einerstelle), jeweils 4 Bit.

Schritt 1: Den programmierbaren Zähler erstellen (Prog_counter.v)

Die Datei Prog_counter.v ist das Herzstück. Sie muss den 7-Bit-Zähler aus Counter.v instanziieren und Logik hinzufügen, die den Zähler bei Erreichen von max_count stoppt. Der run-Eingang steuert den Modus: Bei run=0 wird der Zähler auf 0 zurückgesetzt und der Benutzer kann max_count setzen; bei run=1 zählt der Zähler von 0 bis max_count und stoppt dann.

Hier ein möglicher Code für Prog_counter.v:

module Prog_counter(
    input wire clk,
    input wire rst_n,
    input wire run,
    input wire [6:0] max_count,
    output wire [6:0] count_out
);

wire enable;
wire [6:0] counter_out;

// Instanziierung des 7-Bit-Zählers
Counter #(.WIDTH(7)) u_counter (
    .clk(clk),
    .rst_n(rst_n),
    .enable(enable),
    .count(counter_out)
);

// Steuerlogik
assign enable = run && (counter_out < max_count);
assign count_out = (run) ? counter_out : 7'd0;

endmodule

Erklärung: Der Zähler ist nur aktiv, wenn run=1 und der aktuelle Zählerstand kleiner als max_count ist. Sobald der Zähler max_count erreicht, wird enable auf 0 gesetzt und der Zähler stoppt. Bei run=0 wird count_out auf 0 gesetzt (über die assign-Anweisung). Der Reset rst_n setzt den Zähler asynchron zurück.

Schritt 2: Den Top-Level-BCD-Zähler erstellen (Final_bcd_counter.v)

In Final_bcd_counter.v werden der programmierbare Zähler und der Binär-zu-BCD-Konverter verbunden. Der Konverter erwartet einen 7-Bit-Eingang und liefert zwei 4-Bit-Ausgänge für Zehner und Einer.

module Final_bcd_counter(
    input wire clk,
    input wire rst_n,
    input wire run,
    input wire [6:0] max_count,
    output wire [3:0] digit_2,  // Zehner
    output wire [3:0] digit_1   // Einer
);

wire [6:0] count_bin;

// Programmierbarer Zähler
Prog_counter u_prog (
    .clk(clk),
    .rst_n(rst_n),
    .run(run),
    .max_count(max_count),
    .count_out(count_bin)
);

// Binär-zu-BCD-Konverter (vorgegeben)
Binary_bcd u_bcd (
    .binary_in(count_bin),
    .ones(digit_1),
    .tens(digit_2)
);

endmodule

Damit ist die Logik komplett. Jetzt müssen wir das Design testen.

Schritt 3: Testbench und Simulation

Eine gründliche Testbench ist entscheidend, um alle Bedingungen zu prüfen. Hier ein Beispiel für eine Testbench, die verschiedene Szenarien abdeckt:

module tb_Final_bcd_counter;

reg clk;
reg rst_n;
reg run;
reg [6:0] max_count;
wire [3:0] digit_2, digit_1;

Final_bcd_counter uut (
    .clk(clk),
    .rst_n(rst_n),
    .run(run),
    .max_count(max_count),
    .digit_2(digit_2),
    .digit_1(digit_1)
);

// Taktgenerator (100 MHz -> 10 ns Periode)
initial clk = 0;
always #5 clk = ~clk;

// Testablauf
initial begin
    // Initialisierung
    rst_n = 0;
    run = 0;
    max_count = 7'd50;
    #20 rst_n = 1;
    
    // Test 1: Set-Modus (run=0) - Ausgabe sollte 0 sein
    #20;
    if (digit_2 !== 4'd0 || digit_1 !== 4'd0) $display("Test 1 fehlgeschlagen: Erwarte 00");
    
    // Test 2: Starte Zähler mit max_count=50
    run = 1;
    #500; // Warte 50 Taktzyklen (bei 100 MHz: 500 ns)
    // Nach 50 Takten sollte der Zähler bei 50 stehen und dann stoppen
    if (digit_2 !== 4'd5 || digit_1 !== 4'd0) $display("Test 2 fehlgeschlagen: Erwarte 50");
    
    // Test 3: Weiterzählen sollte nicht passieren
    #100;
    if (digit_2 !== 4'd5 || digit_1 !== 4'd0) $display("Test 3 fehlgeschlagen: Zähler sollte bei 50 stoppen");
    
    // Test 4: Zurücksetzen und neuen Max-Wert setzen
    run = 0;
    max_count = 7'd99;
    #20;
    if (digit_2 !== 4'd0 || digit_1 !== 4'd0) $display("Test 4 fehlgeschlagen: Erwarte 00 nach Reset");
    
    // Test 5: Zähler bis 99 laufen lassen
    run = 1;
    #1000; // 100 Taktzyklen
    if (digit_2 !== 4'd9 || digit_1 !== 4'd9) $display("Test 5 fehlgeschlagen: Erwarte 99");
    
    // Test 6: max_count Änderung während run=1 sollte ignoriert werden
    max_count = 7'd10;
    #100;
    if (digit_2 !== 4'd9 || digit_1 !== 4'd9) $display("Test 6 fehlgeschlagen: Änderung ignoriert");
    
    $display("Alle Tests bestanden!");
    $finish;
end

endmodule

Simuliere die Testbench mit einem Tool wie ModelSim oder Vivado. Überprüfe die Waveforms, um sicherzustellen, dass der Zähler korrekt startet, stoppt und die BCD-Ausgabe stimmt.

Schritt 4: Synthese und FPGA-Implementierung

Für die Synthese auf dem Anvyl-Board benötigst du eine UCF-Datei, die die Pins zuweist. Hier ein Auszug:

NET "clk" LOC = "D11";  # 100 MHz Takt
NET "rst_n" LOC = "T10"; # z.B. Button
NET "run" LOC = "P8";   # SW7
NET "max_count[6]" LOC = "P7"; # SW6
... # Weitere Switches und LEDs
NET "digit_2[3]" LOC = "R4"; # LED7
... # Weitere LEDs

Nach der Synthese lade das Bitstream auf das FPGA und teste die Funktionalität. Die LEDs zeigen die BCD-Ziffern an: LED7–LED4 für die Zehnerstelle, LED3–LED0 für die Einerstelle.

Häufige Fehler und Debugging-Tipps

  • Zähler stoppt nicht: Überprüfe die Enable-Logik. Stelle sicher, dass enable nur aktiv ist, wenn count < max_count.
  • BCD-Ausgabe falsch: Der Konverter Binary_bcd arbeitet korrekt, aber stelle sicher, dass der Eingang count_bin 7 Bit breit ist.
  • Reset-Verhalten: Bei run=0 muss der Zähler auf 0 gesetzt werden, nicht nur der Ausgang.

Fazit

Du hast nun einen voll funktionsfähigen programmierbaren BCD-Zähler in Verilog entworfen. Dieses Projekt ist ein wichtiger Schritt zum Verständnis von FPGA-Design und sequentiellen Schaltungen. Die Fähigkeiten, die du hier lernst – wie das Kombinieren vorgegebener Module und das Schreiben eigener Steuerlogik – sind in der Industrie gefragt, sei es für Embedded Systems, KI-Beschleuniger oder Gaming-Hardware.

Vergiss nicht, deine Ergebnisse auf dem FPGA zu demonstrieren und ein Video für den Bericht zu erstellen. Viel Erfolg im Lab 3!