Arithmetische Operationen in VHDL verstehen

VHDL (VHSIC Hardware Description Language) bietet robuste Unterstützung für die Implementierung arithmetischer Operationen und ist damit ein Eckpfeiler für die Entwicklung digitaler Systeme wie Prozessoren, digitale Signalprozessoren (DSPs) und Steuergeräte. Arithmetische Blöcke wie Addierer, Subtrahierer und Multiplikatoren sind grundlegende Bausteine, und es ist entscheidend, wie man sie effizient implementiert, sowohl für die Simulation als auch für die Synthese. Dieser Artikel bietet eine gründliche, produktionsorientierte Erkundung dieser Operationen, die die Auswahl von Datentypen, die Bedienernutzung, Architekturstile und gängige Optimierungstechniken abdeckt.

Bevor man sich mit spezifischen Komponenten beschäftigt, ist es wichtig, die beiden wichtigsten VHDL-Pakete zu verstehen, die Arithmetik ermöglichen: ieee.numeric std und ieee.std logic unsigned Der empfohlene Ansatz für neue Designs ist die Verwendung numeric std, die unsigned und signed-Datentypen definiert und überlastete Arithmetikoperatoren bereitstellt. Dieses Paket gewährleistet ein konsistentes Verhalten über Simulatoren und Synthesizer hinweg und vermeidet die Fallstricke, die mit dem älteren std logic arith-Paket verbunden sind.

Alle Codebeispiele in diesem Artikel werden mit IEEE 1076-2008 kompatiblen VHDL- und Ziel-Xylinx- oder Intel (Altera) FPGA-Geräten geschrieben, aber die Konzepte gelten für jeden digitalen Designfluss.

Additionen in VHDL

Addition ist die grundlegendste arithmetische Operation. In VHDL können Addierer auf verschiedenen Abstraktionsebenen implementiert werden: Verhaltens- (unter Verwendung des Operators +), Datenfluss (unter Verwendung von gleichzeitiger Signalzuweisung) oder strukturelle (Untermauerung von Komponenten auf niedrigerer Ebene). Für die meisten praktischen Designs bietet Verhaltensmodellierung mit numeric std die beste Balance zwischen Lesbarkeit und Syntheseeffizienz.

Einfacher Ripple-Carry Adder

Ein Ripple-Carry-Addierer kettet Full-Addierer zusammen, wobei die Übertragung jedes Bits den Carry-In des nächsthöheren Bits einspeist. Das folgende Beispiel zeigt einen 4-Bit-unsignierten Ripple-Carry-Addierer mit einem Verhaltensprozess:

library IEEE;
use IEEE.STD_LOGIC_1164.ALL;
use IEEE.NUMERIC_STD.ALL;

entity adder4bit is
 Port (
 A : in unsigned(3 downto 0);
 B : in unsigned(3 downto 0);
 Sum : out unsigned(3 downto 0);
 Cout : out std_logic
 );
end adder4bit;

architecture Behavioral of adder4bit is
begin
 process(A, B)
 variable temp_sum : unsigned(4 downto 0);
 begin
 temp_sum := ('0' & A) + ('0' & B);
 Sum <= temp_sum(3 downto 0);
 Cout <= temp_sum(4);
 end process;
end Behavioral;

Schlüsselpunkte:

  • Die Verkettung ('0' & A) erweitert die Eingänge auf 5 Bits und erfasst den Durchsatz.
  • Die Verwendung von unsigned direkt mit dem +-Operator ist synthetisierbar; das Tool schließt auf die entsprechende Addierlogik (Ripple-Carry, Carry-Lookahead oder LUT-basiert in FPGAs).
  • Die Prozesssensitivitätsliste enthält alle Eingangssignale, so dass die Ausgabe bei jeder Änderung sofort aktualisiert wird (kombinationales Verhalten).

Carry-Lookahead-Addierer (CLA)

Für breitere Addierer (z. B. 16-Bit oder 32-Bit) führen Ripple-Carry-Addierer eine signifikante Verzögerung aufgrund der Übertragungsausbreitung ein. Ein Carry-Lookahead-Addierer reduziert diese Verzögerung durch die parallele Erzeugung von Übertragungssignalen. Die Implementierung eines vollständigen CLA in VHDL erfordert die Beschreibung der Generierungslogik (gi = aii und die Propagierung (pi XOR bi). In der Praxis schließen moderne Synthesewerkzeuge jedoch automatisch auf CLA-Strukturen für breite Addierer, wenn der +-Operator verwendet wird. Das folgende Beispiel zeigt einen 16-Bit-Addierer, der wahrscheinlich

entity adder16bit is
 Port (
 A : in unsigned(15 downto 0);
 B : in unsigned(15 downto 0);
 Sum : out unsigned(15 downto 0);
 CO : out std_logic
 );
end adder16bit;

architecture Behavioral of adder16bit is
 signal temp : unsigned(16 downto 0);
begin
 temp <= ('0' & A) + ('0' & B);
 Sum <= temp(15 downto 0);
 CO <= temp(16);
end Behavioral;

Addierer mit Overflow Detection

Bei Verwendung von signierten-Nummern tritt ein Überlauf auf, wenn das Vorzeichen des Ergebnisses nicht mit dem erwarteten Vorzeichen übereinstimmt, das auf den Eingaben basiert.

entity signed_adder is
 Port (
 A : in signed(7 downto 0);
 B : in signed(7 downto 0);
 Sum : out signed(7 downto 0);
 Overflow: out std_logic
 );
end signed_adder;

architecture Behavioral of signed_adder is
 signal extended_sum : signed(8 downto 0);
begin
 extended_sum <= (A(7) & A) + (B(7) & B); -- sign extend
 Sum <= extended_sum(7 downto 0);
 Overflow <= extended_sum(8) XOR extended_sum(7); -- sign mismatch
end Behavioral;

Subtrahierer in VHDL

Subtraktion kann entweder durch direkte Verwendung des Operators - oder durch Hinzufügen der beiden Komplemente des Subtrahends zum Minuend implementiert werden.

Direkter Verhaltens-Subtraktor

Der einfachste Subtrahierer verwendet den Operator - mit unsignierten oder signierten Typen. Für unsignierte Subtraktion kann das Ergebnis negativ werden, wenn B > A; in solchen Fällen müssen wir einen Unterfluss (Kredit) erkennen. Das folgende Beispiel gibt sowohl die Differenz als auch ein Kredit-Flag zurück:

entity subtractor4bit is
 Port (
 A : in unsigned(3 downto 0);
 B : in unsigned(3 downto 0);
 Diff : out unsigned(3 downto 0);
 Borrow : out std_logic
 );
end subtractor4bit;

architecture Behavioral of subtractor4bit is
 signal temp_diff : signed(4 downto 0);
begin
 process(A, B)
 begin
 temp_diff <= signed('0' & A) - signed('0' & B);
 if temp_diff(4) = '1' then
 Borrow <= '1';
 else
 Borrow <= '0';
 end if;
 Diff <= unsigned(temp_diff(3 downto 0));
 end process;
end Behavioral;

Beachten Sie die Umwandlung in signed für die Zwischenberechnung; dies ermöglicht die korrekte Handhabung negativer Differenzen.

Subtraktor mit Two's Complement

Alternativ können Sie die Subtraktion implementieren, indem Sie das Zweierkomplement von B hinzufügen. Diese Technik ist üblich, wenn Sie einen vorhandenen Addierer in einer ALU wiederverwenden. Das Zweierkomplement von B wird berechnet als (nicht B) + 1.

signal B_comp : unsigned(3 downto 0);
signal sum_with_borrow : unsigned(4 downto 0);

B_comp <= (not B) + 1; -- two's complement
sum_with_borrow <= ('0' & A) + ('0' & B_comp);
Diff <= sum_with_borrow(3 downto 0);
Borrow <= not sum_with_borrow(4); -- borrow asserted if carry out is 0

Beide Methoden sind synthetisch freundlich; Wählen Sie diejenige, die den architektonischen Vorlieben Ihres Designs entspricht.

Vergleich und Subtraktion

Wenn man die Differenz zwischen den beiden Differenzen betrachtet, kann man ohne einen dedizierten Komparator feststellen, ob A > B, A < B oder A = B ist.

Multiplikatoren in VHDL

Multiplikation ist ressourcenintensiver als Addition oder Subtraktion. VHDL unterstützt den Operator * für unsignierte und signierte Typen, was einen kombinatorischen Multiplikator ergibt. Für größere Bitbreiten können jedoch kombinatorische Multiplikatoren signifikante Logik verbrauchen und lange Ausbreitungsverzögerungen haben. Sequenzielle und Pipeline-Implementierungen sind oft für High-Speed-Designs notwendig.

Kombinierter Multiplikator

Ein 4-Bit-Multiplikator mit dem Operator * ist trivial:

entity multiplier4bit is
 Port (
 A : in unsigned(3 downto 0);
 B : in unsigned(3 downto 0);
 Product : out unsigned(7 downto 0)
 );
end multiplier4bit;

architecture Behavioral of multiplier4bit is
begin
 Product <= A * B;
end Behavioral;

Dies führt zu einem kombinatorischen Multiplikator, der in einem FPGA typischerweise unter Verwendung von DSP-Slices (wie Xilinx DSP48-Blöcke) oder LUT-basierter Logik implementiert wird. Für Breiten bis zu 18 Bit können die meisten FPGA-Tools die Multiplikation auf einen einzelnen DSP-Slice abbilden. Für breitere Multiplikatoren kann das Synthesewerkzeug mehrere DSP-Slices kombinieren oder Soft Logic verwenden.

Sequentieller Multiplikator (Shift-and-Add)

Bei flächenbeschränkten Designs oder wenn eine kombinatorische Verzögerung nicht akzeptabel ist, kann ein sequentieller Multiplikator verwendet werden, der über Bits iteriert wird. Der klassische Schiebe- und Addieralgorithmus multipliziert zwei N-Bit-Zahlen über N Taktzyklen.

entity sequential_multiplier is
 Port (
 clk : in std_logic;
 reset : in std_logic;
 start : in std_logic;
 A : in unsigned(3 downto 0);
 B : in unsigned(3 downto 0);
 done : out std_logic;
 Product : out unsigned(7 downto 0)
 );
end sequential_multiplier;

architecture Behavioral of sequential_multiplier is
 signal multiplicand : unsigned(7 downto 0);
 signal multiplier : unsigned(3 downto 0);
 signal product_reg : unsigned(7 downto 0);
 signal count : integer range 0 to 4;
 signal busy : std_logic;
begin
 process(clk)
 begin
 if rising_edge(clk) then
 if reset = '1' then
 count <= 0;
 busy <= '0';
 product_reg <= (others => '0');
 done <= '0';
 elsif start = '1' and busy = '0' then
 multiplicand <= "0000" & A; -- left-aligned 4-bit multiplicand
 multiplier <= B;
 product_reg <= (others => '0');
 count <= 0;
 busy <= '1';
 done <= '0';
 elsif busy = '1' then
 if multiplier(0) = '1' then
 product_reg <= product_reg + multiplicand;
 end if;
 multiplicand <= multiplicand(6 downto 0) & '0'; -- shift left
 multiplier <= '0' & multiplier(3 downto 1); -- shift right
 count <= count + 1;
 if count = 3 then
 busy <= '0';
 done <= '1';
 end if;
 end if;
 end if;
 end process;
 Product <= product_reg;
end Behavioral;

Dieses Design verwendet eine L-Bit-Addition pro Taktzyklus (4 Zyklen für 4-Bit-Eingänge), spart Platz, opfert jedoch Durchsatz und Latenz.

Pipeline-Multiplikator

Bei Anwendungen mit hohem Durchsatz werden durch einen Pipeline-Multiplikator zwischen den Stufen der kombinatorischen Multiplikation Register eingefügt. Viele FPGA-Synthese-Tools können automatisch einen Multiplikator pipen, wenn Sie Pipeline-Register hinzufügen.

-- Pipelined unsigned 4x4 multiplier (2-stage pipeline)
architecture Pipelined of multiplier4bit is
 signal stage1_prod : unsigned(7 downto 0);
 signal stage1_A, stage1_B : unsigned(3 downto 0);
 signal stage2_prod : unsigned(7 downto 0);
begin
 process(clk)
 begin
 if rising_edge(clk) then
 stage1_A <= A;
 stage1_B <= B;
 stage1_prod <= stage1_A * stage1_B; -- first stage
 stage2_prod <= stage1_prod; -- second stage
 Product <= stage2_prod;
 end if;
 end process;
end Pipelined;

Dieser einfache zweistufige Ansatz verdoppelt den Durchsatz (ein Ergebnis pro Takt nach anfänglicher Latenz) und fügt nur eine zusätzliche Registerschicht hinzu.

Verwendung von DSP-Slices

Moderne FPGAs enthalten gehärtete DSP-Scheiben, die für Multiplikation und Akkumulation konfiguriert sind. In VHDL schließt der Operator * diese Blöcke oft automatisch ab. Um DSP-Inferenz zu gewährleisten, folgen Sie den Herstellerrichtlinien: Halten Sie Operanden innerhalb der Schichtbreite (z. B. 18x18, 18x25), vermeiden Sie große relationale Operatoren auf dem Ergebnis und verwenden Sie die entsprechende Breite. Für Xilinx 7-Serie können Sie auch die DSP48E1 direkt instanziieren, aber die Verwendung des Operators wird für die Portabilität bevorzugt.

Xilinx Vivado Synthesis Guide

Optimierungstechniken und Syntheseüberlegungen

Bei der Implementierung arithmetischer Operationen in VHDL beeinflussen mehrere Faktoren die Qualität der Ergebnisse:

  • Datenbreite: Verwenden Sie die kleinste notwendige Breite, um die Logik zu reduzieren.
  • Synthesis Attributes: Wenden Sie Attribute wie keep, use dsp oder mult style an, um die Zuordnung zu beeinflussen.
  • Ressourcen-Sharing: Wenn mehrere Operationen denselben Addierer oder Multiplikator verwenden, sollten Sie die Wiederverwendung von Hardware über eine gemeinsame Komponente oder einen einzelnen Rechenblock mit multiplexierten Eingängen in Betracht ziehen.
  • Pipelining: Legen Sie Register ein, um die zeitlichen Einschränkungen zu erfüllen.
  • Signed vs Unsigned: Use signed for signed operations; the synthesis tool will treat the MSB as a sign bit, affecting the arithmetic logic inferred.
  • Carry Chains: Für Wide Adder ist die Carry Chain eine dedizierte Ressource in FPGAs. Stellen Sie sicher, dass Ihr Synthese-Tool die Kette nicht durch die Verwendung unangemessener Zwischensignale unterbricht.

Kombinieren: ALU Beispiel

Um zu veranschaulichen, wie Addierer, Subtrahierer und Multiplikatoren in ein größeres Design integriert sind, sollten Sie eine einfache Arithmetik-Logik-Einheit (ALU) in Betracht ziehen, die zwei 8-Bit-Werte basierend auf einem Auswahlsignal addieren, subtrahieren oder multiplizieren kann:

entity alu is
 Port (
 A, B : in signed(7 downto 0);
 op : in std_logic_vector(1 downto 0); -- "00": add, "01": sub, "10": mul
 result : out signed(15 downto 0)
 );
end alu;

architecture Behavioral of alu is
begin
 process(A, B, op)
 begin
 case op is
 when "00" => result <= resize(A + B, 16); -- sign extend
 when "01" => result <= resize(A - B, 16);
 when "10" => result <= A * B;
 when others => result <= (others => '0');
 end case;
 end process;
end Behavioral;

Diese ALU verwendet das gleiche Ergebnisregister wieder und kombiniert die drei Operationen, wobei jede Operation als separater Block mit dem Ausgang eines Multiplexers ausgeführt wird. Je nach Zielgerät kann der Multiplizierer der kritische Pfad sein.

Verwenden von IP-Cores für komplexe Arithmetik

Für fortgeschrittene Operationen (z. B. Gleitkomma, Quadratwurzel, Modulo) oder wenn maximale Leistung erforderlich ist, ist es ratsam, vom Hersteller bereitgestellte IP-Kerne zu verwenden. Diese sind hoch optimiert und haben verifizierte Simulationsmodelle. In VHDL instanziieren Sie einen IP-Kern als Komponente, indem Sie Ihre Signale auf seine Ports abbilden.

  • Xilinx Floating-Point Operator für Add/Sub/Multiply/Divide im IEEE 754-Format.
  • Altera (Intel) ALTMULT ADD für Multiplikations-Add-Operationen.
  • Gitterteiler für die Fixpunkt-Teilung.

Die Verwendung von IP-Kernen minimiert das Risiko und führt oft zu einer besseren Leistung als handcodierte Äquivalente.

Intel FPGA IP Cores Guide

Test und Verifizierung

Die Simulation ist für arithmetische Designs entscheidend. Schreibe Testbenches, die Eckfälle ausbilden: Überlauf, Null, Maximalwerte und Mischzeichen (für signierte Typen). Teste bei Multiplikatoren alle Kombinationen der kleinsten Eingaben, um den Algorithmus zu verifizieren.

signal A, B : unsigned(3 downto 0);
signal Sum : unsigned(3 downto 0);
signal Cout : std_logic;
...
A <= "1100"; B <= "0011"; wait for 10 ns;
assert (Sum = "1111" and Cout = '0')
 report "Adder failed for 12 + 3" severity error;

Für größere Designs sollten Sie zufällige Reiz- und Golden-Modelle in Skriptsprachen (Python, Tcl) verwenden, um Testvektoren zu erzeugen.

VHDL Testbench Techniken (SynthWorks)

Schlussfolgerung

Die Implementierung von arithmetischen Operationen in VHDL ist eine Mischung aus dem Verständnis von digitaler Arithmetik, der kompetenten Verwendung von Datentypen und Operatoren und dem Bewusstsein für das Verhalten von Synthesewerkzeugen. Adder und Subtrahierer sind einfach, wenn Sie numeric std verwenden, während Multiplikatoren eine sorgfältige Berücksichtigung von Leistung und Bereich erfordern. Durch die Verwendung von Verhaltensbeschreibungen erzielen Sie schnell Arbeitsdesigns und durch die Anwendung von Techniken wie Pipelining, Ressourcenfreigabe und DSP-Inferenz optimieren Sie für reale Hardware. Mit den bereitgestellten Beispielen und Richtlinien sind Sie ausgestattet, um robuste arithmetische Einheiten für jedes digitale System zu bauen.

Für weitere Informationen lesen Sie das IEEE VHDL Language Reference Manual und die herstellerspezifische Dokumentation über arithmetische Inferenz.

IEEE Std 1076-2008 VHDL Language Reference Manual