Sök…


Introduktion

I det här ämnet föreslår vi en enkel metod för att korrekt utforma enkla digitala kretsar med VHDL. Metoden är baserad på grafiska blockscheman och en lätt att komma ihåg princip:

Tänk hårdvara först, kod VHDL nästa

Det är avsett för nybörjare inom digital hårdvarudesign med VHDL, med en begränsad förståelse för syntesens semantik för språket.

Anmärkningar

Digital hårdvarudesign med VHDL är enkel, även för nybörjare, men det finns några viktiga saker att veta och en liten uppsättning regler att följa. Verktyget som används för att transformera en VHDL-beskrivning i digital hårdvara är en logisk synthesizer. Semantiken för VHDL-språket som används av logiska synthesizers skiljer sig ganska mycket från simuleringssemantiken som beskrivs i Language Reference Manual (LRM). Ännu värre: det är inte standardiserat och varierar mellan syntesverktyg.

Den föreslagna metoden introducerar flera viktiga begränsningar för enkelhetens skull:

  • Inga nivåutlösade spärrar.
  • Kretsarna är synkrona vid stigningen på en enda klocka.
  • Ingen asynkron återställning eller inställning.
  • Ingen multipel enhet på upplösta signaler.

Exempel på blockschema , först i en serie av 3, presenterar kort grunderna för digital hårdvara och föreslår en kort lista med regler för att utforma ett blockschema för en digital krets. Reglerna hjälper till att garantera en enkel översättning till VHDL-kod som simulerar och syntetiserar som förväntat.

Kodningsexemplet förklarar översättningen från ett blockdiagram till VHDL-kod och illustrerar den på en enkel digital krets.

Slutligen visar John Cooleys designtävlingsexempel hur man använder den föreslagna metoden på ett mer komplext exempel på digital krets. Den utarbetar också de införda begränsningarna och slappnar av några av dem.

Blockdiagram

Digital hårdvara är byggd av två typer av hårdvaruprioritet:

  • Kombinatoriska grindar (inverterare, och, eller, xor, 1-bitars fulla adderare, 1-bitars multiplexer ...) Dessa logiska grindar utför en enkel boolesk beräkning på sina ingångar och ger en utgång. Varje gång en av sina ingångar ändras börjar de sprida elektriska signaler och efter en kort fördröjning stabiliseras utgången till det resulterande värdet. Förökningsfördröjningen är viktig eftersom den är starkt relaterad till hastigheten med vilken den digitala kretsen kan köra, det vill säga dess maximala klockfrekvens.
  • Minneselement (spärrar, D-flip-flops, RAMs ...). I motsats till de kombinerande logiska grindarna, reagerar inte minneelementet omedelbart på förändring av någon av deras ingångar. De har dataingångar, kontrollingångar och datautgångar. De reagerar på en viss kombination av kontrollingångar, inte på någon förändring av deras dataingångar. Den stigande kantutlösade D-flip-flop (DFF) har till exempel en klockingång och en dataingång. På varje stigande kant på klockan samplas datainmatningen och kopieras till datautgången som förblir stabil fram till nästa stigande kant på klockan, även om dataingången ändras emellan.

En digital hårdvarukrets är en kombination av kombinatorisk logik och minneselement. Minneselement har flera roller. En av dem är att tillåta återanvändning av samma kombinatoriska logik för flera på varandra följande operationer på olika data. Kretsar som använder detta kallas ofta sekventiella kretsar . Figuren nedan visar ett exempel på en sekventiell krets som ackumulerar heltal med hjälp av samma kombinatoriska adderare, tack vare ett stigande kant utlöst register. Det är också vårt första exempel på ett blockschema.

En sekventiell krets

Rörfoder är en annan vanlig användning av minneselement och grunden för många mikroprocessorarkitekturer. Den syftar till att öka klockfrekvensen för en krets genom att dela upp en komplex behandling i följd av enklare operationer och att parallellisera genomförandet av flera på varandra följande behandling:

Rörfoder för en komplex kombinatorisk bearbetning

Blockschemat är en grafisk representation av den digitala kretsen. Det hjälper dig att fatta rätt beslut och få en god förståelse av den övergripande strukturen innan kodning. Det motsvarar de rekommenderade preliminära analysfasema i många mjukvarudesignmetoder. Erfarna designers hoppar ofta över denna designfas, åtminstone för enkla kretsar. Om du emellertid är en nybörjare inom digital hårdvarukonstruktion och om du vill koda en digital krets i VHDL, bör du använda de tio enkla reglerna nedan för att rita ditt blockschema hjälpa dig att få rätt:

  1. Omge din ritning med en stor rektangel. Detta är gränsen för din krets. Allt som passerar denna gräns är en ingångs- eller utgångsport. VHDL-enheten kommer att beskriva denna gräns.
  2. Tydligt separata kantutlösta register (t.ex. fyrkantiga block) från kombinatorisk logik (t.ex. runda block). I VHDL kommer de att översättas till processer men av två mycket olika slag: synkron och kombinatorisk.
  3. Använd inte nivåutlösade spärrar, använd endast stigande kantutlösade register. Denna begränsning kommer inte från VHDL, som är perfekt användbar för att spärra modeller. Det är bara ett rimligt råd för nybörjare. Spärrar behövs mindre ofta och deras användning innebär många problem som vi förmodligen bör undvika, åtminstone för våra första design.
  4. Använd samma klocka för alla dina stigande utlösade register. Återigen är denna begränsning här för enkelhetens skull. Den kommer inte från VHDL, som är perfekt användbar för att modellera flerklockssystem. Namnge clock . Det kommer från utsidan och är en inmatning av alla kvadratiska block och bara dem. Om du vill, representerar inte ens klockan, det är samma för alla fyrkantiga block och du kan lämna det implicit i ditt diagram.
  5. Representera kommunikationen mellan block med namngivna och orienterade pilar. För det block som en pil kommer från, är pilen en utgång. För det block som en pil går till, är pilen en inmatning. Alla dessa pilar kommer att bli portar i VHDL-enheten om de korsar den stora rektangeln eller signaler från VHDL-arkitekturen.
  6. Pilar har ett enda ursprung men de kan ha flera destinationer. Om en pil hade flera ursprung skulle vi faktiskt skapa en VHDL-signal med flera drivrutiner. Detta är inte helt omöjligt men kräver särskild omsorg för att undvika kortslutningar. Vi kommer därför att undvika detta för nu. Om en pil har flera destinationer, gaffla pilen så många gånger som behövs. Använd prickar för att skilja mellan anslutna och icke-anslutna korsningar.
  7. Vissa pilar kommer utanför den stora rektangeln. Dessa är enhetens ingångsportar. En inmatningspil kan inte heller vara utgången från något av dina block. Detta verkställs av VHDL-språket: ingångsportarna för en enhet kan läsas men inte skrivas. Detta är igen för att undvika kortslutningar.
  8. Vissa pilar går utanför. Dessa är utgångsportarna. I VHDL-versioner före 2008 kan utgångsportarna för en enhet skrivas men inte läsas. En utgångspil måste således ha ett enda ursprung och en enda destination: utsidan. Inga gafflar på utgångspilar, en utgångspil kan inte heller vara ingången till ett av dina block. Om du vill använda en utgångspil som inmatning för några av dina block, sätter du in ett nytt runt block för att dela upp det i två delar: den interna, med så många gafflar som du vill, och utpilen som kommer från den nya blockerar och går utanför. Det nya blocket blir ett enkelt kontinuerligt uppdrag i VHDL. Ett slags transparent namnbyte. Eftersom VHDL 2008 ouptut-portar också kan läsas.
  9. Alla pilar som inte kommer eller går från / till utsidan är interna signaler. Du kommer att förklara dem alla i VHDL-arkitekturen.
  10. Varje cykel i diagrammet måste innehålla minst ett kvadratisk block. Detta beror inte på VHDL. Det kommer från de grundläggande principerna för digital hårdvarudesign. Kombinationsslingor ska absolut undvikas. Förutom i mycket sällsynta fall ger de inga användbara resultat. Och en cykel i blockdiagrammet som endast skulle omfatta runda block skulle vara en kombinatorisk slinga.

Glöm inte att kontrollera den sista regeln noggrant, det är lika viktigt som de andra men det kan vara lite svårare att verifiera.

Om du absolut inte behöver funktioner som vi uteslutit för nu, som spärrar, flera klockor eller signaler med flera drivrutiner, bör du enkelt rita ett blockschema över din krets som följer de 10 reglerna. Om inte är problemet förmodligen den krets du vill ha, inte med VHDL eller den logiska syntesen. Och det betyder förmodligen att kretsen du vill ha inte är digital hårdvara.

Att tillämpa de 10 reglerna på vårt exempel på en sekventiell krets skulle leda till ett blockschema som:

Omarbetat blockschema över sekvenskretsen

  1. Den stora rektangeln runt diagrammet korsas av 3 pilar som representerar ingångs- och utgångsportarna för VHDL-enheten.
  2. Blockdiagrammet har två runda (kombinerande) block - adderaren och det utgående döpsnamnet - och ett kvadratiskt (synkron) block - registret.
  3. Den använder endast kantutlöst register.
  4. Det finns bara en klocka med namnet clock och vi använder bara dess stigande kant.
  5. Blockschemat har fem pilar, en med en gaffel. De motsvarar två interna signaler, två ingångsportar och en utgångsport.
  6. Alla pilar har ett ursprung och en destination utom pilen som heter Sum som har två destinationer.
  7. Data_in och Clock pilarna är våra två ingångsportar. De produceras inte från våra egna block.
  8. Data_out pilen är vår utgångsport. För att vara kompatibla med VHDL-versioner före 2008, har vi lagt till ett extra namnbyte (runt) block mellan Sum och Data_out . Så Data_out har exakt en källa och en destination.
  9. Sum och Next_sum är våra två interna signaler.
  10. Det finns exakt en cykel i diagrammet och den innehåller ett kvadratisk block.

Vårt blockschema överensstämmer med de 10 reglerna. Kodningsexemplet kommer att beskriva hur man översätter denna typ av blockdiagram i VHDL.

Kodning

Det här exemplet är det andra i en serie av 3. Om du inte gjorde det, läs först blockschemaexemplet .

Med ett blockschema som överensstämmer med de 10 reglerna (se Exempel på blockschema ) blir VHDL-kodningen enkel:

  • den stora omgivande rektangeln blir VHDL-enheten,
  • interna pilar blir VHDL-signaler och deklareras i arkitekturen,
  • varje kvadratisk block blir en synkron process i arkitekturkroppen,
  • varje runda block blir en kombinatorisk process i arkitekturorganet.

Låt oss illustrera detta på blockschemat för en sekventiell krets:

En sekventiell krets

VHDL-modellen för en krets består av två kompilationsenheter:

  • Enheten som beskriver kretsens namn och dess gränssnitt (portnamn, vägbeskrivning och typer). Det är en direkt översättning av den stora omgivande rektangeln på blockdiagrammet. Förutsatt att uppgifterna är heltal, och clock använder VHDL-typen bit (endast två värden: '0' och '1' ), kan enheten i vår sekventiella krets vara:
entity sequential_circuit is
  port(
    Data_in:  in  integer;
    Clock:    in  bit;
    Data_out: out integer
  );
end entity sequential_circuit;
  • Arkitekturen som beskriver internets krets (vad den gör). Det är här de interna signalerna deklareras och där alla processer inställs. Skelettet för arkitekturen i vår sekventiella krets kan vara:
architecture ten_rules of sequential_circuit is
  signal Sum, Next_sum: integer;
begin
  <...processes...>
end architecture ten_rules;

Vi har tre processer att lägga till i arkitekturkroppen, en synkron (fyrkantig block) och två kombinatoriska (runda block).

En synkron process ser ut så här:

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

där i1, i2,..., ix är alla pilar som kommer in i motsvarande kvadratblock i diagrammet och o1, ..., ox är alla pilar som matar ut motsvarande kvadratblock i diagrammet. Absolut ingenting ska ändras, förutom naturligtvis namnen på signalerna. Ingenting. Inte ens en enda karaktär.

Den synkrona processen enligt vårt exempel är således:

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

Som informellt kan översättas till: om clock ändras, och först då, om förändringen är en stigande kant ( '0' till '1' ), tilldela värdet på signalen Next_sum till signal Sum .

En kombinatorisk process ser ut så här:

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;

där i1, i2,..., in är alla pilar som kommer in i motsvarande runda block i diagrammet. allt och inte mer. Vi kommer inte att glömma någon pil och vi ska inte lägga till något annat i listan.

v1, ..., vy är variabler som vi kan behöva för att förenkla processens kod. De har exakt samma roll som i alla andra nödvändiga programmeringsspråk: inneha tillfälliga värden. De måste absolut tilldelas innan de läses. Om vi inte garanterar detta kommer processen inte längre att vara kombinatorisk eftersom den modellerar typ av minneselement för att behålla värdet på vissa variabler från en processutförande till nästa. Detta är anledningen till vi := <default_value_for_vi> i början av processen. Observera att <default_value_for_vi> måste vara konstanter. Om inte, om de är uttryck, kan vi av misstag använda variabler i uttryck och läsa en variabel innan vi tilldelar den.

o1, ..., om är alla pilar som visar motsvarande runda block i ditt diagram. allt och inte mer. De måste absolut tilldelas minst en gång under processutförandet. Eftersom VHDL-styrstrukturerna ( if , case ...) mycket lätt kan förhindra att en utsignal tilldelas, rekommenderar vi starkt att tilldela var och en av dem, villkorslöst, med ett konstant värde <default_value_for_oi> i början av processen. På detta sätt, även om ett if uttalande maskerar en signaltilldelning, kommer det ändå att ha fått ett värde.

Absolut ingenting ska ändras till detta VHDL-skelett, förutom namnen på variablerna, om några, namnen på ingångarna, namnen på utgångarna, värdena på <default_value_for_..> konstanter och <statements> . Glöm inte en enda standardvärde tilldelning, om du gör syntesen kommer att sluta oönskade minneselement (troligen spärrar) och resultatet blir inte det du ursprungligen ville ha.

I vårt exempel på sekventiell krets är den kombinerande adderprocessen:

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

Som informellt kan översättas till: om Sum eller Data_in (eller båda) ändrar tilldelar värdet 0 för att signalera Next_sum och sedan tilldela det igen värde Sum + Data_in .

Eftersom den första tilldelningen (med det konstanta standardvärdet 0 ) omedelbart följs av en annan tilldelning som skriver över den, kan vi förenkla:

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

Den andra kombinationsprocessen motsvarar det runda blocket som vi lagt till på en utgångspil med mer än en destination för att uppfylla VHDL-versioner före 2008. Dess kod är helt enkelt:

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

Av samma anledning som med den andra kombinatoriska processen kan vi förenkla den som:

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

Den fullständiga koden för sekvenskretsen är:

-- 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;

Obs! Vi kan skriva de tre processerna i valfri ordning, det skulle inte förändra något till slutresultatet i simulering eller i syntes. Detta beror på att de tre processerna är samtidiga uttalanden och VHDL behandlar dem som om de verkligen var parallella.

John Cooley designtävling

Detta exempel härrör direkt från John Cooley designtävling på SNUG'95 (Synopsys Users Group-möte). Tävlingen var avsedd att motsätta sig VHDL- och Verilog-designers på samma designproblem. Vad John hade i åtanke var förmodligen att avgöra vilket språk som var det mest effektiva. Resultaten var att 8 av de 9 Verilog-designarna lyckades genomföra designtävlingen, men ingen av de 5 VHDL-designarna kunde. Förhoppningsvis kommer vi att göra ett mycket bättre jobb med den föreslagna metoden.

Specifikationer

Vårt mål är att utforma i enkel syntetiserbar VHDL (enhet och arkitektur) en synkron upp-för-3, ned-för-5, lastbar, modul 512-räknare, med bärutgång, låneutgång och paritetsutgång. Räknaren är en 9 bitar osignerad räknare så att den sträcker sig mellan 0 och 511. Gränssnittspecifikationen för räknaren anges i följande tabell:

namn Bit-bredd Riktning Beskrivning
KLOCKA 1 Inmatning Master klocka; räknaren är synkroniserad i stigningskanten på CLOCK
DI 9 Inmatning Datainmatningsbuss; räknaren laddas med DI när UPP och NED båda är låga
UPP 1 Inmatning Upp-för-räknekommando; när UP är hög och NED är låg ökar räknarna med 3, och lindar sig runt dess maximala värde (511)
NER 1 Inmatning Ned-för-5 räknekommando; när NED är högt och UPP är lågt räknar dekrementen med 5, lindar runt dess minimivärde (0)
CO 1 Produktion Utför signal; hög endast när man räknar upp över det maximala värdet (511) och därmed lindar sig runt
BO 1 Produktion Låna ut signal; hög endast när man räknar ner under minimivärdet (0) och därmed lindas runt
DO 9 Produktion Utgångsbuss; räknarens nuvarande värde; när UPP och NED är båda höga behåller räknaren sitt värde
PO 1 Produktion Parity out signal; hög när räknarens nuvarande värde innehåller ett jämnt antal 1

När man räknar upp utöver sitt maximala värde eller när man räknar ner under sitt lägsta värde räknas räknaren runt:

Motströmvärde UPP NER Räknar ut nästa värde Nästa CO Nästa BO Nästa PO
x 00 DI 0 0 paritet (DI)
x 11 x 0 0 paritet (x)
0 ≤ x ≤ 508 10 x + 3 0 0 paritet (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 paritets (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

Blockdiagram

Baserat på dessa specifikationer kan vi börja utforma ett blockschema. Låt oss först representera gränssnittet:

Det externa gränssnittet

Vår krets har 4 ingångar (inklusive klockan) och 4 utgångar. Nästa steg består i att bestämma hur många register och kombinatoriska block vi kommer att använda och vad deras roller kommer att vara. För detta enkla exempel kommer vi att ägna ett kombinatoriskt block till beräkningen av nästa värde på räknaren, genomförandet och utlåningen. Ett annat kombinatoriskt block kommer att användas för att beräkna nästa värde på pariteten ut. Räknarens aktuella värden, utmatningen och utlåningen lagras i ett register medan det aktuella värdet för paritet ut lagras i ett separat register. Resultatet visas på figuren nedan:

Två kombinatoriska block och två register

Kontrollera att blockschemat överensstämmer med våra 10 designregler görs snabbt:

  1. Vårt externa gränssnitt representeras korrekt av den stora omgivande rektangeln.
  2. Våra 2 kombinationsblock (runda) och våra 2 register (kvadrat) är tydligt separerade.
  3. Vi använder bara stigande kantutlösade register.
  4. Vi använder bara en klocka.
  5. Vi har 4 interna pilar (signaler), 4 ingångspilar (ingångsportar) och 4 utgångspilar (utgångsportar).
  6. Ingen av våra pilar har flera ursprung. Tre har flera destinationer ( clock , ncnt och do ).
  7. Ingen av våra fyra ingångspilar är en utgång från våra interna block.
  8. Tre av våra utgångspilar har exakt ett ursprung och en destination. Men do har två destinationer: utsidan och en av våra kombinatoriska block. Detta bryter mot regel 8 och måste fixas genom att infoga ett nytt kombinatoriskt block om vi vill följa VHDL-versioner före 2008:

Ett extra kombinationsblock

  1. Vi har nu exakt 5 interna signaler ( cnt , nco , nbo , ncnt och npo ).
  2. Det finns bara en cykel i diagrammet, bildad av cnt och ncnt . Det finns ett fyrkantigt block i cykeln.

Kodning i VHDL-versioner före 2008

Att översätta vårt blockschema i VHDL är enkelt. Räknarens nuvarande värde varierar från 0 till 511, så vi kommer att använda en 9-bitars bit_vector att representera den. Den enda subtiliteten kommer från behovet av att utföra bitvis (som att beräkna pariteten) och aritmetiska operationer på samma data. Det standarda numeric_bit paketet för bibliotek ieee löser detta: det förklarar en unsigned typ med exakt samma deklaration som bit_vector och överbelaster de aritmetiska operatörerna så att de tar någon blandning av unsigned och heltal. För att beräkna genomförandet och upplåningen kommer vi att använda ett 10-bitars unsigned tillfälligt värde.

Biblioteksdeklarationerna och enheten:

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;

Arkitekturets skelett är:

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;

Var och en av våra fem block är modellerad som en process. De synkrona processer som motsvarar våra två register är mycket enkla att koda. Vi använder helt enkelt det mönster som föreslås i kodningsexemplet . Registeret som lagrar paritetsflaggan, till exempel, är kodat:

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

och det andra registret som lagrar co , bo och cnt :

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

Den nya namnet på kombinatorisk process är också mycket enkel:

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

Paritetsberäkningen kan använda en variabel och en enkel slinga:

  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;

Den sista kombineringsprocessen är den mest komplexa av alla men att tillämpa den föreslagna översättningsmetoden helt enkelt gör det också lätt:

  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;

Observera att de två synkrona processerna också kan slås samman och att en av våra kombinatoriska processer kan förenklas i en enkel samtidig signaltilldelning. Den fullständiga koden, med bibliotek och paketdeklarationer, och med de föreslagna förenklingarna är som följer:

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;

Gå lite längre

Den föreslagna metoden är enkel och säker men den förlitar sig på flera begränsningar som kan avslappnas.

Hoppa över blockdiagramritningen

Erfarna designers kan hoppa över ritningen av ett blockschema för enkla mönster. Men de tror fortfarande hårdvara först. De ritar i huvudet istället för på ett papper men de fortsätter på något sätt att rita.

Använd asynkrona återställningar

Det finns omständigheter där asynkrona återställningar (eller uppsättningar) kan förbättra en designkvalitet. Den föreslagna metoden stöder endast synkrona återställningar (det vill säga återställningar som beaktas vid klockans stigande kanter):

  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;

Versionen med asynkron återställning modifierar vår mall genom att lägga till återställningssignalen i känslighetslistan och genom att ge den högsta prioritet:

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

Slå samman flera enkla processer

Vi har redan använt detta i den slutliga versionen av vårt exempel. Att slå samman flera synkrona processer, om de alla har samma klocka, är trivialt. Att slå samman flera kombinerande processer i en är också trivialt och är bara en enkel omorganisation av blockschemat.

Vi kan också slå samman vissa kombinatoriska processer med synkrona processer. Men för att göra detta måste vi gå tillbaka till vårt blockschema och lägga till en elfte regel:

  1. Gruppera flera runda block och minst ett kvadratisk block genom att rita en kapsling runt dem. Omslut också de pilar som kan vara. Låt inte en pil passera gränsen för kapslingen om den inte kommer eller går från / till utanför kapslingen. När detta är gjort, titta på alla utgångspilarna i kapslingen. Om någon av dem kommer från ett runt block i inneslutningen eller också är en inmatning i inneslutningen, kan vi inte slå samman dessa processer i en synkron process. Annars kan vi.

I vårt motexempel, till exempel, kunde vi inte gruppera de två processerna i den röda kapslingen i följande figur:

Processer som inte kan slås samman

eftersom ncnt är en utgång från inneslutningen och dess ursprung är ett runt (kombinatoriskt) block. Men vi kunde gruppera:

Processer som kan slås samman

Den interna npo skulle bli värdelös och den resulterande processen skulle vara:

  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;

som också kan slås samman med den andra synkrona processen:

  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;

Grupperingen kan till och med vara:

Mer gruppering

Leder till den mycket enklare arkitekturen:

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;

med två processer (den samtidiga signaltilldelningen av do är en kortfattning för motsvarande process). Lösningen med bara en process lämnas som en övning. Se upp, det väcker intressanta och subtila frågor.

Gå ännu längre

Nivåutlösade spärrar, fallande klockkanter, flera klockor (och resynkroniserare mellan klockdomäner), flera drivrutiner för samma signal etc. är inte onda. De är ibland användbara. Men att lära sig använda dem och hur man undviker de tillhörande fallgroparna går långt bortom denna korta introduktion till digital hårdvarudesign med VHDL.

Kodning i VHDL 2008

VHDL 2008 introducerade flera modifieringar som vi kan använda för att ytterligare förenkla vår kod. I det här exemplet kan vi dra nytta av två modifieringar:

  • utgångsportar kan läsas, vi behöver inte cnt signalen längre,
  • den unary xor operatören kan användas för att beräkna pariteten.

VHDL 2008-koden kan vara:

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
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow