Table of Contents
Comprendre les opérations arithmétiques en VHDL
VHDL (VHSIC Hardware Description Language) fournit un support solide pour la mise en œuvre des opérations arithmétiques, en faisant une pierre angulaire pour la conception de systèmes numériques tels que les processeurs, les processeurs de signaux numériques (DSP) et les unités de contrôle. Les blocs arithmétiques comme les adders, les soustracteurs et les multiplicateurs sont des éléments de base, et savoir les mettre en œuvre efficacement est essentiel pour la simulation et la synthèse.
Avant de plonger dans des composants spécifiques, il est essentiel de comprendre les deux paquets VHDL clés qui permettent l'arithmétique : ieee.numeric std et ieee.std logic unsigned. L'approche recommandée pour les nouveaux modèles est d'utiliser numeric std[, qui définit unsigned[ et signed]signed][et fournit des opérateurs arithmétiques surchargés.
Tous les exemples de code dans cet article sont écrits en utilisant IEEE 1076-2008 compatible VHDL et cible Xilinx ou les périphériques Intel (Altera) FPGA, mais les concepts s'appliquent à tout flux de conception numérique.
Addeurs dans VHDL
Dans VHDL, vous pouvez implémenter des adders à différents niveaux d'abstraction : comportemental (en utilisant l'opérateur +), dataflow (en utilisant l'assignation simultanée de signal), ou structural (en installant des composants de niveau inférieur). Pour la plupart des modèles pratiques, la modélisation comportementale avec numeric std fournit le meilleur équilibre de l'efficacité de lecture et de synthèse.
Addeur simple pour jarretelles
Un rallonge-porteur de chaînes d'addition complète, où le déroulement de chaque bit alimente le transport du bit suivant. L'exemple suivant montre un rallonge-porteur non signé de 4 bits en utilisant un processus comportemental:
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;
Points clés:
- La concaténation ('0' & A) étend les entrées à 5 bits, captant le résultat.
- En utilisant non signé directement avec l'opérateur + est synthétisé; l'outil en déduit la logique d'ajout appropriée (porteur-pivot, port-lookahead ou LUT-basé dans les FPGA).
- La liste de sensibilité du processus comprend tous les signaux d'entrée, assurant les mises à jour de sortie immédiatement sur tout changement (comportement combinational).
Additif de port-lookahead (CLA)
Pour les additifs à entraînement à hélices plus larges (p. ex., 16 bits ou 32 bits), les additifs à entraînement à hélices présentent un retard important en raison de la propagation du chariot. Un additif à portage réduit ce retard en générant des signaux de transport en parallèle. La mise en œuvre d'un CLA complet dans VHDL exige de décrire la génération ([gi = a[i & b[i et de propager ([pii] = a[]]i] XOR bi]]]]]]]]]]]]]]]]]]]]]]]]]
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;
Addeur avec détection de dépassement
Lorsque vous utilisez des nombres signés, le débordement se produit lorsque le signe du résultat ne correspond pas au signe attendu basé sur les entrées. La détection du débordement est essentielle dans les UTA du processeur. L'extrait suivant montre un addeur signé avec détection du débordement:
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;
Sous-tracteurs dans VHDL
La soustraction peut être mise en œuvre soit par l'utilisation directe de l'opérateur -], soit en ajoutant le complément de deux de la soustrahend au minuend. Bien que la modélisation comportementale soit simple, il est essentiel de comprendre la propagation et la manipulation des résultats négatifs.
Sous-tracteur comportemental direct
Le soustracteur le plus simple utilise l'opérateur - avec des types non signés ou signés. Pour la soustraction non signée, le résultat peut devenir négatif si B > A; dans ces cas, nous devons détecter un sous-débit (emprunt). L'exemple suivant renvoie à la fois la différence et un drapeau d'emprunt:
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;
Notez la conversion en signé[ pour le calcul intermédiaire; cela permet une bonne manipulation des différences négatives. Le bit le plus significatif (temp diff(4)) agit comme le drapeau d'emprunt.
Sous-tracteur utilisant le complément de deux
Vous pouvez également implémenter la soustraction en ajoutant le complément de deux de B. Cette technique est courante lors de la réutilisation d'un adder existant dans une UTA. Le complément de deux de B est calculé comme (pas B) + 1. Voici le concept :
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
Les deux méthodes sont faciles à synthétiser; sélectionnez celle qui correspond aux préférences architecturales de votre conception.
Comparaison et soustraction
En examinant l'emprunt ou le signe de la différence, vous pouvez déterminer si A > B, A < B ou A = B sans comparateur dédié. Par exemple, après soustraction, si le résultat est zéro (tous les bits 0), les entrées sont égales. Si l'emprunt/sign est 1, puis A < B.
Multiplicateurs en VHDL
La multiplication est plus importante en ressources que l'addition ou la soustraction. VHDL prend en charge l'opérateur * pour les types non signés et signés, ce qui entraîne un multiplicateur mixte. Cependant, pour les largeurs de bits plus grandes, les multiplicateurs combinés peuvent consommer une logique importante et ont des retards de propagation longs.
Multiplieur combiné
Un multiplicateur 4 bits utilisant l'opérateur * est 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;
Cela implique un multiplicateur combiné, qui dans un FPGA est généralement implémenté en utilisant des tranches DSP dédiées (comme les blocs Xilinx DSP48) ou une logique basée sur LUT. Pour des largeurs allant jusqu'à 18 bits, la plupart des outils FPGA peuvent mapper la multiplication sur une seule tranche DSP. Pour des multiplicateurs plus larges, l'outil de synthèse peut combiner plusieurs tranches DSP ou utiliser une logique souple.
Multiplicateur séquentiel (Shift-and-Add)
Pour les conceptions à effet de surface ou lorsque le retard combiné est inacceptable, un multiplicateur séquentiel qui itère sur les bits peut être utilisé. L'algorithme classique de décalage et d'ajout multiplie deux nombres N-bit sur les cycles d'horloge N. Ci-dessous est un exemple simplifié (4-bit multiplicateur, non signé, avec des signaux de contrôle omis pour la clarté):
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;
Cette conception utilise un complément L-bit par cycle d'horloge (4 cycles pour les entrées 4 bits). Il permet d'économiser la surface mais sacrifie le débit et la latence.
Multiplieur pipelinier
Pour les applications à haut débit, un multiplicateur en pipeline insère des registres entre les étapes de la multiplication combinée. De nombreux outils de synthèse FPGA peuvent automatiquement pipelineer un multiplicateur lorsque vous ajoutez des registres de pipeline. Par exemple, en utilisant une pour ... générer une boucle ou une insertion manuelle de l'étape :
-- 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;
Cette approche simple à deux étapes double le débit (un résultat par horloge après la latence initiale) tout en ajoutant seulement une couche de registre supplémentaire. Plus de étapes peuvent être ajoutées pour les fréquences d'horloges plus élevées.
Utilisation des tranches DSP
Dans la série VHDL, l'opérateur * en déduit souvent automatiquement ces blocs. Pour assurer l'inférence de la DSP, suivez les directives du fournisseur : gardez les opérandes dans la largeur de la tranche (p. ex. 18x18, 18x25), évitez les grands opérateurs relationnels sur le résultat et utilisez la largeur appropriée. Pour Xilinx 7-série, vous pouvez aussi activer directement la DSP48E1 primitive, mais l'utilisation de l'opérateur est préférée pour la portabilité.
Xilinx Vivado Guide de synthèse
Techniques d'optimisation et considérations de synthèse
Lors de la mise en œuvre d'opérations arithmétiques en VHDL, plusieurs facteurs influent sur la qualité des résultats:
- Largeur des données: Utilisez la plus petite largeur nécessaire pour réduire la logique. Par exemple, si les entrées sont 5 bits, utilisez non signé(4 descendant à 0).
- Synthèse Attributs:[ Appliquer des attributs comme maintenir[, use dsp[, ou mult style pour influencer la cartographie. Par exemple:
- Partage des ressources:[ Si plusieurs opérations utilisent le même adder ou multiplicateur, envisager de réutiliser du matériel via un composant partagé ou un bloc arithmétique unique avec des entrées multiplexées.
- Pipelining:[ Insérez des registres pour respecter les contraintes de temps. Pour les chaînes additives, équilibrez le placement du registre pour éviter les longues voies combinatoires.
- Signé vs Non signé: Utiliser signé[ pour les opérations signées; l'outil de synthèse traitera l'ESM comme un signe bit, affectant la logique arithmétique déduite.
- Chaînes de transport:[ Pour les gros additifs, la chaîne de transport est une ressource dédiée dans les FPGA. Assurez-vous que votre outil de synthèse ne brise pas la chaîne en utilisant des signaux intermédiaires inappropriés.
Combiner les opérations : exemple de UTA
Pour illustrer comment les adders, les soustracteurs et les multiplicateurs s'intègrent dans un design plus large, considérez une simple unité logique arithmétique (UAL) qui peut ajouter, soustraire ou multiplier deux valeurs de 8 bits basées sur un signal sélectionné :
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;
Cette ALU réutilise le même registre de résultats et combine les trois opérations. En synthèse, chaque opération est implémentée comme un bloc séparé, avec la sortie sélectionnée par un multiplexeur. Selon le périphérique cible, le multiplicateur peut être le chemin critique.
Utilisation de cœurs IP pour complexe arithmétique
Pour les opérations avancées (p. ex., point flottant, racine carrée, modulo) ou lorsque des performances maximales sont nécessaires, il est conseillé d'utiliser des cœurs IP fournis par le fournisseur. Ceux-ci sont hautement optimisés et ont des modèles de simulation vérifiés.
- Exploiteur en point flottant Xilinx pour ajouter/sous/multiplier/diviser en format IEEE 754.
- Altera (Intel) ALTMULT ADD pour les opérations de multiplication-ajout.
- Diviseur de latte pour la division en points fixes.
L'utilisation de cœurs IP minimise les risques et entraîne souvent de meilleures performances que les équivalents codés à la main.
Essais et vérification
La simulation est essentielle pour les modèles arithmétiques. Écrire des testbenches qui exercent des cas de coin : débordement, zéro, valeurs maximales et signes mixtes (pour les types signés). Pour les multiplicateurs, tester toutes les combinaisons des plus petites entrées pour vérifier l'algorithme. Utilisez la déclaration assert pour vérifier les résultats attendus. Exemple d'extrait de testbench pour un adder 4 bits :
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;
Pour les modèles plus grands, envisager d'utiliser des stimulus aléatoires et des modèles dorés dans les langages de script (Python, Tcl) pour générer des vecteurs de test.
Techniques de test de banc d'essai VHDL (SynthWorks)
Conclusion
La mise en œuvre d'opérations arithmétiques dans VHDL est un mélange de compréhension de l'arithmétique numérique, d'utilisation compétente des types de données et des opérateurs, et de sensibilisation au comportement des outils de synthèse. Les addeurs et les sous-tracteurs sont simples lorsque l'on utilise numeric std, tandis que les multiplicateurs nécessitent une attention particulière aux performances et à la zone.
Pour plus de détails, consultez le Manuel de référence linguistique de l'IEEE VHDL et la documentation propre au fournisseur sur l'inférence arithmétique.
Manuel de référence linguistique de la VHDL, TIEE Std 1076-2008