vhdl
Digital hårdvarudesign med VHDL i ett nötskal
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.
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:
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:
- 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.
- 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.
- 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.
- 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. - 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.
- 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.
- 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.
- 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.
- 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.
- 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:
- Den stora rektangeln runt diagrammet korsas av 3 pilar som representerar ingångs- och utgångsportarna för VHDL-enheten.
- Blockdiagrammet har två runda (kombinerande) block - adderaren och det utgående döpsnamnet - och ett kvadratiskt (synkron) block - registret.
- Den använder endast kantutlöst register.
- Det finns bara en klocka med namnet
clock
och vi använder bara dess stigande kant. - Blockschemat har fem pilar, en med en gaffel. De motsvarar två interna signaler, två ingångsportar och en utgångsport.
- Alla pilar har ett ursprung och en destination utom pilen som heter
Sum
som har två destinationer. -
Data_in
ochClock
pilarna är våra två ingångsportar. De produceras inte från våra egna block. -
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 mellanSum
ochData_out
. SåData_out
har exakt en källa och en destination. -
Sum
ochNext_sum
är våra två interna signaler. - 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:
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-typenbit
(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:
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:
Kontrollera att blockschemat överensstämmer med våra 10 designregler görs snabbt:
- Vårt externa gränssnitt representeras korrekt av den stora omgivande rektangeln.
- Våra 2 kombinationsblock (runda) och våra 2 register (kvadrat) är tydligt separerade.
- Vi använder bara stigande kantutlösade register.
- Vi använder bara en klocka.
- Vi har 4 interna pilar (signaler), 4 ingångspilar (ingångsportar) och 4 utgångspilar (utgångsportar).
- Ingen av våra pilar har flera ursprung. Tre har flera destinationer (
clock
,ncnt
ochdo
). - Ingen av våra fyra ingångspilar är en utgång från våra interna block.
- 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:
- Vi har nu exakt 5 interna signaler (
cnt
,nco
,nbo
,ncnt
ochnpo
). - Det finns bara en cykel i diagrammet, bildad av
cnt
ochncnt
. 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:
- 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:
eftersom ncnt
är en utgång från inneslutningen och dess ursprung är ett runt (kombinatoriskt) block. Men vi kunde gruppera:
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:
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;