Zoeken…


Invoering

In dit onderwerp stellen we een eenvoudige methode voor om eenvoudige digitale circuits met VHDL correct te ontwerpen. De methode is gebaseerd op grafische blokdiagrammen en een gemakkelijk te onthouden principe:

Denk eerst aan hardware, codeer vervolgens VHDL

Het is bedoeld voor beginners in digitaal hardwareontwerp met behulp van VHDL, met een beperkt begrip van de synthese-semantiek van de taal.

Opmerkingen

Digitaal hardware-ontwerp met behulp van VHDL is eenvoudig, zelfs voor beginners, maar er zijn een paar belangrijke dingen om te weten en een kleine reeks regels waaraan u zich moet houden. De tool die wordt gebruikt om een VHDL-beschrijving in digitale hardware te transformeren, is een logische synthesizer. De semantiek van de VHDL-taal die wordt gebruikt door logische synthesizers verschilt nogal van de simulatie-semantiek die wordt beschreven in de Language Reference Manual (LRM). Erger nog: het is niet gestandaardiseerd en varieert tussen synthesetools.

Voor de eenvoud introduceert de voorgestelde methode verschillende belangrijke beperkingen:

  • Geen niveau-geactiveerde vergrendelingen.
  • De circuits zijn synchroon op de stijgende flank van een enkele klok.
  • Geen asynchrone reset of instelling.
  • Geen meervoudige schijf op opgeloste signalen.

De Block diagram voorbeeld, eerste van een serie van 3, kort presenteert de basisprincipes van digitale hardware en stelt een korte lijst van regels om een blok diagram van een digitaal circuit te ontwerpen. De regels helpen om een eenvoudige vertaling naar VHDL-code te garanderen die simuleert en synthetiseert zoals verwacht.

Het coderingsvoorbeeld verklaart de vertaling van een blokdiagram naar VHDL-code en illustreert deze op een eenvoudig digitaal circuit.

Ten slotte laat het voorbeeld van de John Cooley's ontwerpwedstrijd zien hoe de voorgestelde methode kan worden toegepast op een meer complex voorbeeld van een digitaal circuit. Het gaat ook dieper in op de geïntroduceerde beperkingen en ontspant sommige ervan.

Blokdiagram

Digitale hardware is opgebouwd uit twee soorten hardware-primitieven:

  • Combinatorische poorten (omvormers en, of, xor, 1-bit volledige adders, 1-bit multiplexers ...) Deze logische poorten voeren een eenvoudige Booleaanse berekening uit op hun ingangen en produceren een uitgang. Telkens wanneer een van hun ingangen verandert, beginnen ze elektrische signalen door te geven en na een korte vertraging stabiliseert de uitgang zich naar de resulterende waarde. De voortplantingsvertraging is belangrijk omdat deze sterk gerelateerd is aan de snelheid waarmee het digitale circuit kan lopen, dat wil zeggen de maximale klokfrequentie.
  • Geheugenelementen (vergrendelingen, D-flip-flops, RAM's ...). In tegenstelling tot de combinatorische logische poorten reageren geheugenelementen niet onmiddellijk op de verandering van een van hun ingangen. Ze hebben data-ingangen, besturingsingangen en data-uitgangen. Ze reageren op een bepaalde combinatie van besturingsingangen, niet op een wijziging van hun gegevensingangen. De stijgende flank getriggerde D-flip-flop (DFF) heeft bijvoorbeeld een klokinvoer en een gegevensinvoer. Op elke stijgende flank van de klok wordt de gegevensinvoer bemonsterd en gekopieerd naar de gegevensuitvoer die stabiel blijft tot de volgende stijgende flank van de klok, zelfs als de gegevensinvoer tussendoor verandert.

Een digitaal hardwarecircuit is een combinatie van combinatorische logica en geheugenelementen. Geheugenelementen hebben verschillende rollen. Een ervan is het mogelijk maken dezelfde combinatorische logica opnieuw te gebruiken voor verschillende opeenvolgende bewerkingen op verschillende gegevens. Circuits die dit gebruiken worden vaak sequentiële circuits genoemd . De onderstaande afbeelding toont een voorbeeld van een sequentieel circuit dat gehele waarden verzamelt met behulp van dezelfde combinatorische opteller, dankzij een geactiveerd register met stijgende flank. Het is ook ons eerste voorbeeld van een blokdiagram.

Een sequentieel circuit

Pipe-lining is een ander veelgebruikt gebruik van geheugenelementen en de basis van veel micro-processorarchitecturen. Het heeft tot doel de klokfrequentie van een circuit te verhogen door een complexe verwerking op te splitsen in een opeenvolging van eenvoudiger bewerkingen en de uitvoering van verschillende opeenvolgende verwerking parallel te laten lopen:

Buisvoering van een complexe combinatorische verwerking

Het blokdiagram is een grafische weergave van het digitale circuit. Het helpt bij het nemen van de juiste beslissingen en het verkrijgen van een goed begrip van de algehele structuur alvorens te coderen. Het is het equivalent van de aanbevolen voorlopige analysefasen in veel softwareontwerpmethoden. Ervaren ontwerpers slaan deze ontwerpfase vaak over, althans voor eenvoudige circuits. Als u echter een beginner bent in het ontwerp van digitale hardware en als u een digitaal circuit in VHDL wilt coderen, moet u de volgende 10 eenvoudige regels volgen om uw blokdiagram te tekenen:

  1. Omring uw tekening met een grote rechthoek. Dit is de grens van je circuit. Alles dat deze grens overschrijdt, is een invoer- of uitvoerpoort. De VHDL-entiteit zal deze grens beschrijven.
  2. Scheid duidelijk rand-geactiveerde registers (bijv. Vierkante blokken) van combinatorische logica (bijv. Ronde blokken). In VHDL worden ze vertaald in processen, maar van twee zeer verschillende soorten: synchroon en combinatorisch.
  3. Gebruik geen niveau-getriggerde grendels, gebruik alleen opgaande flank getriggerde registers. Deze beperking komt niet van VHDL, dat perfect bruikbaar is om grendels te modelleren. Het is gewoon een redelijk advies voor beginners. Vergrendelingen zijn minder vaak nodig en het gebruik ervan levert veel problemen op die we waarschijnlijk moeten vermijden, althans voor onze eerste ontwerpen.
  4. Gebruik dezelfde enkele klok voor al uw stijgende edge-geactiveerde registers. Ook hier is deze beperking hier omwille van de eenvoud. Het komt niet van VHDL, dat perfect bruikbaar is om multi-kloksystemen te modelleren. Noem de klok clock . Het komt van buitenaf en is een input voor alle vierkante blokken en alleen voor hen. Stel desgewenst niet eens de klok voor, deze is hetzelfde voor alle vierkante blokken en u kunt deze impliciet in uw diagram laten staan.
  5. Vertegenwoordig de communicatie tussen blokken met benoemde en georiënteerde pijlen. Voor het blok waar een pijl vandaan komt, is de pijl een uitvoer. Voor het blok waar een pijl naartoe gaat, is de pijl een invoer. Al deze pijlen worden poorten van de VHDL-entiteit, als ze de grote rechthoek kruisen, of signalen van de VHDL-architectuur.
  6. Pijlen hebben één oorsprong, maar ze kunnen verschillende bestemmingen hebben. Inderdaad, als een pijl verschillende oorsprong had, zouden we een VHDL-signaal met verschillende stuurprogramma's creëren. Dit is niet helemaal onmogelijk, maar vereist speciale zorg om kortsluiting te voorkomen. We zullen dit dus voorlopig vermijden. Als een pijl meerdere bestemmingen heeft, vork de pijl dan zo vaak als nodig. Gebruik stippen om verbonden en niet-verbonden kruisingen te onderscheiden.
  7. Sommige pijlen komen van buiten de grote rechthoek. Dit zijn de invoerpoorten van de entiteit. Een invoerpijl kan niet ook de uitvoer van uw blokken zijn. Dit wordt afgedwongen door de VHDL-taal: de invoerpoorten van een entiteit kunnen worden gelezen maar niet geschreven. Dit is opnieuw om kortsluiting te voorkomen.
  8. Sommige pijlen gaan naar buiten. Dit zijn de uitvoerpoorten. In VHDL-versies van vóór 2008 kunnen de uitvoerpoorten van een entiteit worden geschreven maar niet gelezen. Een uitvoerpijl moet dus één enkele oorsprong en één enkele bestemming hebben: de buitenkant. Geen vorken op uitvoerpijlen, een uitvoerpijl kan niet ook de invoer van een van uw blokken zijn. Als u een uitvoerpijl wilt gebruiken als invoer voor sommige van uw blokken, voegt u een nieuw rond blok in om het in twee delen te splitsen: het interne blok, met zoveel vorken als u wilt, en de uitvoerpijl die uit de nieuwe komt blokkeren en gaat naar buiten. Het nieuwe blok wordt een eenvoudige continue opdracht in VHDL. Een soort transparant hernoemen. Sinds VHDL 2008 kunnen ook ouptut-poorten worden gelezen.
  9. Alle pijlen die niet van / naar buiten komen of gaan, zijn interne signalen. Je zult ze allemaal in de VHDL-architectuur verklaren.
  10. Elke cyclus in het diagram moet ten minste één vierkant blok bevatten. Dit komt niet door VHDL. Het komt van de basisprincipes van digitaal hardwareontwerp. Combinatielussen moeten absoluut worden vermeden. Behalve in zeer zeldzame gevallen produceren ze geen bruikbaar resultaat. En een cyclus van het blokdiagram dat alleen ronde blokken zou omvatten, zou een combinatorische lus zijn.

Vergeet niet om de laatste regel zorgvuldig te controleren, deze is net zo essentieel als de andere regels, maar het kan wat moeilijker zijn om te verifiëren.

Tenzij u absoluut functies nodig hebt die we voorlopig hebben uitgesloten, zoals vergrendelingen, meerdere klokken of signalen met meerdere stuurprogramma's, moet u eenvoudig een blokdiagram van uw circuit tekenen dat voldoet aan de 10 regels. Als dit niet het geval is, ligt het probleem waarschijnlijk bij het circuit dat u wilt, niet bij VHDL of de logische synthesizer. En het betekent waarschijnlijk dat het circuit dat u zoekt geen digitale hardware is.

Het toepassen van de 10 regels op ons voorbeeld van een sequentieel circuit zou leiden tot een blokdiagram zoals:

Herwerkt blokdiagram van het sequentiële circuit

  1. De grote rechthoek rond het diagram wordt doorkruist door 3 pijlen die de invoer- en uitvoerpoorten van de VHDL-entiteit voorstellen.
  2. Het blokdiagram heeft twee ronde (combinatorische) blokken - de opteller en het uitvoer hernoemde blok - en een vierkant (synchroon) blok - het register.
  3. Het gebruikt alleen edge-triggered registers.
  4. Er is slechts één klok, genaamd clock en we gebruiken alleen de stijgende flank.
  5. Het blokdiagram heeft vijf pijlen, één met een vork. Ze komen overeen met twee interne signalen, twee invoerpoorten en één uitvoerpoort.
  6. Alle pijlen hebben één oorsprong en één bestemming behalve de pijl met de naam Sum die twee bestemmingen heeft.
  7. De pijlen Data_in en Clock zijn onze twee invoerpoorten. Ze zijn geen output van onze eigen blokken.
  8. De pijl Data_out is onze uitvoerpoort. Om compatibel te zijn met VHDL-versies van vóór 2008 hebben we een extra hernoemingsblok (rond) toegevoegd tussen Sum en Data_out . Data_out heeft dus precies één bron en één bestemming.
  9. Sum en Next_sum zijn onze twee interne signalen.
  10. Er is precies één cyclus in de grafiek en deze bestaat uit een vierkant blok.

Ons blokdiagram voldoet aan de 10 regels. Het coderingsvoorbeeld zal gedetailleerd beschrijven hoe dit type blokdiagrammen in VHDL kan worden vertaald.

Coding

Dit voorbeeld is het tweede van een reeks van 3. Als u dit nog niet hebt gedaan, lees dan eerst het blokdiagramvoorbeeld .

Met een blokdiagram dat voldoet aan de 10 voorschriften (zie Blokschema voorbeeld), de VHDL codering wordt eenvoudig:

  • de grote omringende rechthoek wordt de VHDL-entiteit,
  • interne pijlen worden VHDL-signalen en worden verklaard in de architectuur,
  • elk vierkant blok wordt een synchroon proces in de architectuur,
  • elk rond blok wordt een combinatorisch proces in de architectuur.

Laten we dit illustreren op het blokdiagram van een sequentieel circuit:

Een sequentieel circuit

Het VHDL-model van een circuit bestaat uit twee compilatie-eenheden:

  • De entiteit die de naam van het circuit en de interface beschrijft (poortnamen, richtingen en typen). Het is een directe vertaling van de grote omringende rechthoek van het blokdiagram. Ervan uitgaande dat de gegevens gehele getallen zijn en de clock het bit type VHDL gebruikt (alleen twee waarden: '0' en '1' ), zou de entiteit van ons sequentiële circuit kunnen zijn:
entity sequential_circuit is
  port(
    Data_in:  in  integer;
    Clock:    in  bit;
    Data_out: out integer
  );
end entity sequential_circuit;
  • De architectuur die de binnenkant van het circuit beschrijft (wat het doet). Dit is waar de interne signalen worden gedeclareerd en waar alle processen worden gestart. Het skelet van de architectuur van ons sequentiële circuit zou kunnen zijn:
architecture ten_rules of sequential_circuit is
  signal Sum, Next_sum: integer;
begin
  <...processes...>
end architecture ten_rules;

We moeten drie processen toevoegen aan de architectuur, een synchroon (vierkant blok) en twee combinatorische (ronde blokken).

Een synchroon proces ziet er als volgt uit:

process(clock)
begin
  if rising_edge(clock) then
    o1 <= i1;
    ...
    ox <= ix;
  end if;
end process;

waarbij i1, i2,..., ix alle pijlen zijn die het overeenkomstige vierkante blok van het diagram binnenkomen en o1, ..., ox zijn alle pijlen die het overeenkomstige vierkante blok van het diagram uitvoeren. Er zal natuurlijk niets worden veranderd, behalve de namen van de signalen. Niets. Zelfs geen enkel karakter.

Het synchrone proces van ons voorbeeld is dus:

  process(clock)
  begin
    if rising_edge(clock) then
      Sum <= Next_sum;
    end if;
  end process;

Die informeel kan worden vertaald in: als de clock verandert, en alleen dan, als de verandering een stijgende flank is ( '0' tot '1' ), wijs de waarde van het signaal Next_sum om Sum te signaleren.

Een combinatorisch proces ziet er zo uit:

process(i1, i2,... , ix)
  variable v1: <type_of_v1>;
  ...
  variable vy: <type_of_vy>;
begin
  v1 := <default_value_for_v1>;
  ...
  vy := <default_value_for_vy>;
  o1 <= <default_value_for_o1>;
  ...
  oz <= <default_value_for_oz>;
  <statements>
end process;

waarbij i1, i2,..., in alle pijlen zijn die het overeenkomstige ronde blok van het diagram binnenkomen. alles en niet meer. We zullen geen enkele pijl vergeten en we zullen niets anders aan de lijst toevoegen.

v1, ..., vy zijn variabelen die we mogelijk nodig hebben om de code van het proces te vereenvoudigen. Ze hebben precies dezelfde rol als in elke andere gebiedende programmeertaal: tijdelijke waarden behouden. Ze moeten absoluut allemaal zijn toegewezen voordat ze worden gelezen. Als we dit niet garanderen, zal het proces niet langer combinatorisch zijn, omdat het een soort geheugenelementen zal modelleren om de waarde van sommige variabelen van de ene procesuitvoering naar de volgende te behouden. Dit is de reden voor de vi := <default_value_for_vi> -instructies aan het begin van het proces. Merk op dat de <default_value_for_vi> constanten moet zijn. Zo niet, als het uitdrukkingen zijn, kunnen we per ongeluk variabelen in de uitdrukkingen gebruiken en een variabele lezen voordat we deze toewijzen.

o1, ..., om zijn allemaal pijlen die het overeenkomstige ronde blok van uw diagram uitvoeren. alles en niet meer. Ze moeten absoluut allemaal minstens één keer worden toegewezen tijdens de procesuitvoering. Omdat de VHDL-besturingsstructuren ( if , case ...) heel eenvoudig kunnen voorkomen dat een uitgangssignaal wordt toegewezen, raden we ten sterkste aan om ze elk onvoorwaardelijk toe te wijzen met een constante waarde <default_value_for_oi> aan het begin van het proces. Op deze manier heeft het, zelfs als een if instructie een signaaltoewijzing maskeert, toch een waarde ontvangen.

Absoluut niets zal worden gewijzigd in dit VHDL-skelet, behalve de namen van de eventuele variabelen, de namen van de ingangen, de namen van de uitgangen, de waarden van de constanten <default_value_for_..> en <statements> . Vergeet geen enkele standaardwaardetoewijzing, als u dit doet, leidt de synthese tot ongewenste geheugenelementen (hoogstwaarschijnlijk vergrendelingen) en is het resultaat niet wat u in eerste instantie wilde.

In ons voorbeeld sequentieel circuit is het combinatorische optelproces:

  process(Sum, Data_in)
  begin
    Next_sum <= 0;
    Next_sum <= Sum + Data_in;
  end process;

Die informeel kan worden vertaald in: als Sum of Data_in (of beide) veranderen, wijs de waarde 0 toe om Next_sum te signaleren en wijs het dan opnieuw waarde Sum + Data_in .

Omdat de eerste toewijzing (met de constante standaardwaarde 0 ) onmiddellijk wordt gevolgd door een andere toewijzing die deze overschrijft, kunnen we vereenvoudigen:

  process(Sum, Data_in)
  begin
    Next_sum <= Sum + Data_in;
  end process;

Het tweede combinatorische proces komt overeen met het ronde blok dat we hebben toegevoegd aan een uitvoerpijl met meer dan één bestemming om te voldoen aan VHDL-versies van vóór 2008. De code is eenvoudig:

  process(Sum)
  begin
    Data_out <= 0;
    Data_out <= Sum;
  end process;

Om dezelfde reden als bij het andere combinatorische proces kunnen we het vereenvoudigen als:

  process(Sum)
  begin
    Data_out <= Sum;
  end process;

De volledige code voor het sequentiële circuit is:

-- File sequential_circuit.vhd
entity sequential_circuit is
  port(
    Data_in:  in  integer;
    Clock:    in  bit;
    Data_out: out integer
  );
end entity sequential_circuit;

architecture ten_rules of sequential_circuit is
  signal Sum, Next_sum: integer;
begin
  process(clock)
  begin
    if rising_edge(clock) then
      Sum <= Next_sum;
    end if;
  end process;

  process(Sum, Data_in)
  begin
    Next_sum <= Sum + Data_in;
  end process;

  process(Sum)
  begin
    Data_out <= Sum;
  end process;
end architecture ten_rules;

Opmerking: we zouden de drie processen in willekeurige volgorde kunnen schrijven, het zou niets veranderen aan het eindresultaat in simulatie of in synthese. Dit komt omdat de drie processen gelijktijdige verklaringen zijn en VHDL ze behandelt alsof ze echt parallel zijn.

John Cooley's ontwerpwedstrijd

Dit voorbeeld is rechtstreeks afgeleid van de ontwerpwedstrijd van John Cooley op SNUG'95 (vergadering van de Synopsys Users Group). De wedstrijd was bedoeld om VHDL- en Verilog-ontwerpers tegen hetzelfde ontwerpprobleem te bestrijden. Wat John in gedachten had, was waarschijnlijk om te bepalen welke taal het meest efficiënt was. De resultaten waren dat 8 van de 9 Verilog-ontwerpers erin slaagden de ontwerpwedstrijd te voltooien, maar geen van de 5 VHDL-ontwerpers kon dat. Hopelijk zullen we met de voorgestelde methode het veel beter doen.

bestek

Ons doel is om in gewoon synthetiseerbare VHDL (entiteit en architectuur) een synchrone up-by-3, down-by-5, laadbare, modulus 512-teller te ontwerpen, met carry-output, leenoutput en pariteitsoutput. De teller is een 9-bits teller zonder teken, dus deze loopt van 0 tot 511. De interfacespecificatie van de teller wordt gegeven in de volgende tabel:

Naam Bitbreedte Richting Beschrijving
KLOK 1 Invoer Master klok; de teller wordt gesynchroniseerd op de stijgende flank van KLOK
DI 9 Invoer Gegevensinvoerbus; de teller wordt geladen met DI wanneer UP en DOWN beide laag zijn
UP 1 Invoer Up-by-3 count commando; wanneer OMHOOG hoog en OMLAAG laag is, wordt de teller met 3 verhoogd, rond de maximale waarde (511)
DOWN 1 Invoer Down-by-5 count commando; wanneer DOWN hoog is en UP laag is, daalt de teller met 5, rond de minimumwaarde (0)
CO 1 uitgang Signaal uitvoeren; hoog alleen bij het optellen boven de maximale waarde (511) en dus rondwikkelen
BO 1 uitgang Leen signaal uit; hoog alleen bij het aftellen tot onder de minimumwaarde (0) en dus omwikkelen
DOEN 9 uitgang Uitgang bus; de huidige waarde van de teller; wanneer UP en DOWN beide hoog zijn, behoudt de teller zijn waarde
PO 1 uitgang Pariteit uit signaal; hoog wanneer de huidige waarde van de teller een even aantal enen bevat

Bij het aftellen boven de maximale waarde of bij het aftellen tot onder de minimale waarde loopt de teller rond:

Tegenstroomwaarde OP NEER Teller volgende waarde Volgende CO Volgende BO Volgende PO
X 00 DI 0 0 pariteit (DI)
X 11 X 0 0 pariteit (x)
0 ≤ x ≤ 508 10 x + 3 0 0 pariteit (x + 3)
509 10 0 1 0 1
510 10 1 1 0 0
511 10 2 1 0 0
5 ≤ x ≤ 511 01 x-5 0 0 pariteit (x-5)
4 01 511 0 1 0
3 01 510 0 1 1
2 01 509 0 1 1
1 01 508 0 1 0
0 01 507 0 1 1

Blokdiagram

Op basis van deze specificaties kunnen we beginnen met het ontwerpen van een blokdiagram. Laten we eerst de interface weergeven:

De externe interface

Ons circuit heeft 4 ingangen (inclusief de klok) en 4 uitgangen. De volgende stap bestaat uit het beslissen hoeveel registers en combinatorische blokken we zullen gebruiken en wat hun rollen zullen zijn. Voor dit eenvoudige voorbeeld zullen we één combinatorisch blok wijden aan de berekening van de volgende waarde van de teller, het uitvoeren en het lenen. Een ander combinatieblok zal worden gebruikt om de volgende waarde van de pariteit te berekenen. De huidige waarden van de teller, het uitvoeren en het lenen worden opgeslagen in een register, terwijl de huidige waarde van de pariteit wordt opgeslagen in een afzonderlijk register. Het resultaat is te zien in onderstaande figuur:

Twee combinatorische blokken en twee registers

Controleren of het blokdiagram voldoet aan onze 10 ontwerpregels is snel gedaan:

  1. Onze externe interface wordt correct weergegeven door de grote omringende rechthoek.
  2. Onze 2 combinatorische blokken (rond) en onze 2 registers (vierkant) zijn duidelijk gescheiden.
  3. We gebruiken alleen door stijgende flank geactiveerde registers.
  4. We gebruiken slechts één klok.
  5. We hebben 4 interne pijlen (signalen), 4 invoerpijlen (invoerpoorten) en 4 uitvoerpijlen (uitvoerpoorten).
  6. Geen van onze pijlen heeft verschillende oorsprong. Drie hebben verschillende bestemmingen ( clock , ncnt en do ).
  7. Geen van onze 4 invoerpijlen is een uitvoer van onze interne blokken.
  8. Drie van onze uitvoerpijlen hebben precies één oorsprong en één bestemming. Maar do heeft 2 bestemmingen: de buitenkant en een van onze combinatieblokken. Dit is in strijd met regel nummer 8 en moet worden verholpen door een nieuw combinatorisch blok in te voegen als we willen voldoen aan VHDL-versies van vóór 2008:

Een extra combinatorisch blok

  1. We hebben nu precies 5 interne signalen ( cnt , nco , nbo , ncnt en npo ).
  2. Er is slechts één cyclus in het diagram, gevormd door cnt en ncnt . Er is een vierkant blok in de cyclus.

Codering in VHDL-versies van vóór 2008

Het vertalen van ons blokdiagram in VHDL is eenvoudig. De huidige waarde van de teller varieert van 0 tot 511, dus we zullen een 9-bits bit_vector signaal gebruiken om het weer te geven. De enige subtiliteit komt van de noodzaak om bitgewijs (zoals het berekenen van de pariteit) en rekenkundige bewerkingen op dezelfde gegevens uit te voeren. Het standaard numeric_bit pakket van bibliotheek ieee lost dit op: het declareert een unsigned type met exact dezelfde verklaring als bit_vector en overbelast de rekenkundige operatoren zodat ze een mengsel van unsigned en gehele getallen nemen. Om de uitvoering en de uitlening te berekenen, gebruiken we een niet- unsigned tijdelijke waarde van 10 bits.

De bibliotheekverklaringen en de entiteit:

library ieee;
use ieee.numeric_bit.all;

entity cooley is
  port(
        clock: in  bit;
        up:    in  bit;
        down:  in  bit;
        di:    in  bit_vector(8 downto 0);
        co:    out bit;
        bo:    out bit;
        po:    out bit;
        do:    out bit_vector(8 downto 0)
      );
end entity cooley;

Het skelet van de architectuur is:

architecture arc1 of cooley is
  signal cnt:  unsigned(8 downto 0);
  signal ncnt: unsigned(8 downto 0);
  signal nco:  bit;
  signal nbo:  bit;
  signal npo:  bit;
begin
    <...processes...>
end architecture arc1;

Elk van onze 5 blokken wordt gemodelleerd als een proces. De synchrone processen die overeenkomen met onze twee registers zijn zeer eenvoudig te coderen. We gebruiken eenvoudig het patroon dat in het coderingsvoorbeeld wordt voorgesteld. Het register waarin de pariteitsvlag is opgeslagen, is bijvoorbeeld gecodeerd:

  poreg: process(clock)
  begin
    if rising_edge(clock) then
      po <= npo;
    end if;
  end process poreg;

en het andere register waarin co , bo en cnt opgeslagen:

  cobocntreg: process(clock)
  begin
    if rising_edge(clock) then
      co  <= nco;
      bo  <= nbo;
      cnt <= ncnt;
    end if;
  end process cobocntreg;

Het hernoemde combinatoriële proces is ook heel eenvoudig:

  rename: process(cnt)
  begin
    do <= (others => '0');
    do <= bit_vector(cnt);
  end process rename;

De pariteitsberekening kan een variabele en een eenvoudige lus gebruiken:

  parity: process(ncnt)
    variable tmp: bit;
  begin
    tmp := '0';
    npo <= '0';
    for i in 0 to 8 loop
      tmp := tmp xor ncnt(i);
    end loop;
    npo <= not tmp;
  end process parity;

Het laatste combinatorische proces is het meest complexe van allemaal, maar het strikt toepassen van de voorgestelde vertaalmethode maakt het ook eenvoudig:

  u3d5: process(up, down, di, cnt)
    variable tmp: unsigned(9 downto 0);
  begin
    tmp  := (others => '0');
    nco  <= '0';
    nbo  <= '0';
    ncnt <= (others => '0');
    if up = '0' and down = '0' then
      ncnt <= unsigned(di);
    elsif up = '1' and down = '1' then
      ncnt <= cnt;
    elsif up = '1' and down = '0' then
      tmp   := ('0' & cnt) + 3;
      ncnt  <= tmp(8 downto 0);
      nco   <= tmp(9);
    elsif up = '0' and down = '1' then
      tmp   := ('0' & cnt) - 5;
      ncnt  <= tmp(8 downto 0);
      nbo   <= tmp(9);
    end if;
  end process u3d5;

Merk op dat de twee synchrone processen ook kunnen worden samengevoegd en dat een van onze combinatorische processen kan worden vereenvoudigd in een eenvoudige gelijktijdige signaaltoewijzing. De volledige code, met bibliotheek- en pakketverklaringen, en met de voorgestelde vereenvoudigingen is als volgt:

library ieee;
use ieee.numeric_bit.all;

entity cooley is
  port(
        clock: in  bit;
        up:    in  bit;
        down:  in  bit;
        di:    in  bit_vector(8 downto 0);
        co:    out bit;
        bo:    out bit;
        po:    out bit;
        do:    out bit_vector(8 downto 0)
      );
end entity cooley;

architecture arc2 of cooley is
  signal cnt:  unsigned(8 downto 0);
  signal ncnt: unsigned(8 downto 0);
  signal nco:  bit;
  signal nbo:  bit;
  signal npo:  bit;
begin
  reg: process(clock)
  begin
    if rising_edge(clock) then
      co  <= nco;
      bo  <= nbo;
      po  <= npo;
      cnt <= ncnt;
    end if;
  end process reg;

  do <= bit_vector(cnt);

  parity: process(ncnt)
    variable tmp: bit;
  begin
    tmp := '0';
    npo <= '0';
    for i in 0 to 8 loop
      tmp := tmp xor ncnt(i);
    end loop;
    npo <= not tmp;
  end process parity;

  u3d5: process(up, down, di, cnt)
    variable tmp: unsigned(9 downto 0);
  begin
    tmp  := (others => '0');
    nco  <= '0';
    nbo  <= '0';
    ncnt <= (others => '0');
    if up = '0' and down = '0' then
      ncnt <= unsigned(di);
    elsif up = '1' and down = '1' then
      ncnt <= cnt;
    elsif up = '1' and down = '0' then
      tmp   := ('0' & cnt) + 3;
      ncnt  <= tmp(8 downto 0);
      nco   <= tmp(9);
    elsif up = '0' and down = '1' then
      tmp   := ('0' & cnt) - 5;
      ncnt  <= tmp(8 downto 0);
      nbo   <= tmp(9);
    end if;
  end process u3d5;
end architecture arc2;

Iets verder gaan

De voorgestelde methode is eenvoudig en veilig, maar berust op verschillende beperkingen die kunnen worden versoepeld.

Sla de blokdiagramtekening over

Ervaren ontwerpers kunnen de tekening van een blokdiagram overslaan voor eenvoudige ontwerpen. Maar ze denken nog steeds eerst aan hardware. Ze tekenen in hun hoofd in plaats van op een vel papier, maar blijven op de een of andere manier tekenen.

Gebruik asynchrone resets

Er zijn omstandigheden waarin asynchrone resets (of sets) de kwaliteit van een ontwerp kunnen verbeteren. De voorgestelde methode ondersteunt alleen synchrone resets (d.w.z. resets waarmee rekening wordt gehouden bij stijgende flanken van de klok):

  process(clock)
  begin
    if rising_edge(clock) then
      if reset = '1' then
        o <= reset_value_for_o;
      else
        o <= i;
      end if;
    end if;
  end process;

De versie met asynchrone reset wijzigt onze sjabloon door het resetsignaal toe te voegen aan de gevoeligheidslijst en de hoogste prioriteit te geven:

  process(clock, reset)
  begin
    if reset = '1' then
      o <= reset_value_for_o;
    elsif rising_edge(clock) then
      o <= i;
    end if;
  end process;

Verschillende eenvoudige processen samenvoegen

We hebben dit al gebruikt in de definitieve versie van ons voorbeeld. Het is triviaal om verschillende synchrone processen samen te voegen, als ze allemaal dezelfde klok hebben. Het samenvoegen van meerdere combinatorische processen in één is ook triviaal en is slechts een eenvoudige reorganisatie van het blokdiagram.

We kunnen ook enkele combinatorische processen samenvoegen met synchrone processen. Maar om dit te doen moeten we teruggaan naar ons blokdiagram en een elfde regel toevoegen:

  1. Groepeer meerdere ronde blokken en ten minste één vierkant blok door er een omheining omheen te tekenen. Omsluit ook de pijlen die kunnen zijn. Laat een pijl de grens van de behuizing niet overschrijden als deze niet van / naar buiten de behuizing komt of gaat. Zodra dit is gebeurd, kijkt u naar alle uitgangspijlen van de behuizing. Als een van hen uit een rond blok van de behuizing komt of ook een input van de behuizing is, kunnen we deze processen niet in een synchroon proces samenvoegen. Anders kunnen we.

In ons tegenvoorbeeld konden we bijvoorbeeld de twee processen niet groeperen in de rode bijlage van de volgende afbeelding:

Processen die niet kunnen worden samengevoegd

omdat ncnt een uitvoer van de behuizing is en de oorsprong ervan een rond (combinatorisch) blok is. Maar we kunnen groeperen:

Processen die kunnen worden samengevoegd

Het interne signaal npo zou nutteloos worden en het resulterende proces zou zijn:

  poreg: process(clock)
    variable tmp: bit;
  begin
    if rising_edge(clock) then
      tmp := '0';
      for i in 0 to 8 loop
        tmp := tmp xor ncnt(i);
      end loop;
      po <= not tmp;
    end if;
  end process poreg;

die ook kan worden samengevoegd met het andere synchrone proces:

  reg: process(clock)
    variable tmp: bit;
  begin
    if rising_edge(clock) then
      co  <= nco;
      bo  <= nbo;
      cnt <= ncnt;
      tmp := '0';
      for i in 0 to 8 loop
        tmp := tmp xor ncnt(i);
      end loop;
      po <= not tmp;
    end if;
  end process reg;

De groepering kan zelfs zijn:

Meer groepering

Leidt tot de veel eenvoudiger architectuur:

architecture arc5 of cooley is
  signal cnt: unsigned(8 downto 0);
begin
  process(clock)
    variable ncnt: unsigned(9 downto 0);
    variable tmp:  bit;
  begin
    if rising_edge(clock) then
      ncnt := '0' & cnt;
      co   <= '0';
      bo   <= '0';
      if up = '0' and down = '0' then
        ncnt := unsigned('0' & di);
      elsif up = '1' and down = '0' then
        ncnt := ncnt + 3;
        co   <= ncnt(9);
      elsif up = '0' and down = '1' then
        ncnt := ncnt - 5;
        bo   <= ncnt(9);
      end if;
      tmp := '0';
      for i in 0 to 8 loop
        tmp := tmp xor ncnt(i);
      end loop;
      po  <= not tmp;
      cnt <= ncnt(8 downto 0);
    end if;
  end process;

  do <= bit_vector(cnt);
end architecture arc5;

met twee processen (de gelijktijdige signaaltoewijzing van do is een afkorting voor het equivalente proces). De oplossing met slechts één proces blijft over als oefening. Pas op, het roept interessante en subtiele vragen op.

Nog verder gaan

Door niveau geactiveerde vergrendelingen, vallende klokranden, meerdere klokken (en resynchronizers tussen klokdomeinen), meerdere stuurprogramma's voor hetzelfde signaal, etc. zijn niet slecht. Ze zijn soms handig. Maar leren hoe ze te gebruiken en hoe de bijbehorende valkuilen te vermijden gaat veel verder dan deze korte inleiding tot het ontwerp van digitale hardware met VHDL.

Codering in VHDL 2008

VHDL 2008 introduceerde verschillende wijzigingen die we kunnen gebruiken om onze code verder te vereenvoudigen. In dit voorbeeld kunnen we profiteren van 2 wijzigingen:

  • uitvoerpoorten kunnen worden gelezen, we hebben het cnt signaal niet meer nodig,
  • de unary xor operator kan worden gebruikt om de pariteit te berekenen.

De VHDL 2008-code kan zijn:

library ieee;
use ieee.numeric_bit.all;

entity cooley is
  port(
        clock: in  bit;
        up:    in  bit;
        down:  in  bit;
        di:    in  bit_vector(8 downto 0);
        co:    out bit;
        bo:    out bit;
        po:    out bit;
        do:    out bit_vector(8 downto 0)
      );
end entity cooley;

architecture arc6 of cooley is
begin
  process(clock)
    variable ncnt: unsigned(9 downto 0);
  begin
    if rising_edge(clock) then
      ncnt := unsigned('0' & do);
      co   <= '0';
      bo   <= '0';
      if up = '0' and down = '0' then
        ncnt := unsigned('0' & di);
      elsif up = '1' and down = '0' then
        ncnt := ncnt + 3;
        co   <= ncnt(9);
      elsif up = '0' and down = '1' then
        ncnt := ncnt - 5;
        bo   <= ncnt(9);
      end if;
      po <= not (xor ncnt(8 downto 0));
      do <= bit_vector(ncnt(8 downto 0));
    end if;
  end process;
end architecture arc6;


Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow