Diferență între revizuiri ale paginii „CID Seminar 4”
Cbira (discuție | contribuții) |
Cbira (discuție | contribuții) |
||
Linia 1: | Linia 1: | ||
− | + | Numărătorul (''counter'') este unul din cele mai utilizate circuite digitale şi sigur unul dintre cele mai cunoscute în afara lumii electroniştilor. Orice microprocesor are un numărător de program (''program counter'') fără de care nu poate executa programele din calculator. Numărătorul de program generează o secvenţă de adrese de memorie în ordine crescătoare, câte una în fiecare perioadă de ceas, şi din când în când, când se execută un salt în program, sare la o adresă diferită de cea imediat următoare. | |
− | == | + | == Exemplul 1 == |
− | + | Cel mai simplu numărător este registrul cu incrementare. Exercitiul 4 din seminarul 2 v-a cerut descrierea unui circuit combinaţional de incrementare comandată, una din variantele posibile fiind: | |
<syntaxhighlight lang="verilog"> | <syntaxhighlight lang="verilog"> | ||
− | increment | + | module increment( |
input inc, | input inc, | ||
− | input [3: 0] | + | input [3:0] a, |
− | output reg [3: 0] out | + | output reg [3:0] out |
− | ) | + | ); |
− | always @ (*) | + | always @(*) |
− | out=inc? a + 1: a; | + | out = inc ? a + 1 : a; |
endmodule | endmodule | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | Cu minime modificări descrierea de mai sus poate fi transformată într-un numărător cu incrementare comandată. Pentru a genera o secvenţă crescătoare de numere, numărul de la ieşire este readus la intrare. Bucla o putem închide în interiorul modulului, renunţând la intrarea a: | |
<syntaxhighlight lang="verilog"> | <syntaxhighlight lang="verilog"> | ||
− | + | module incorect( | |
input inc, | input inc, | ||
− | output reg [3: 0] out | + | output reg [3:0] out |
− | ) | + | ); |
− | always @ (*) | + | always @(*) |
− | out=inc? out + 1: out; // | + | out = inc ? out + 1 : out; // buclă infinită! |
endmodule | endmodule | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | Închiderea unei bucle peste un circuit combinaţional duce de cele mai multe ori la un circuit instabil, iar în cazul descrierii de mai sus simularea va intra într-o buclă infinită. Reţineţi aşadar o regulă de proiectare: | |
− | <div class=" | + | <div class="regula">Într-un circuit combinaţional sunt interzise buclele! O variabilă nu poate fi folosită niciodată în calculul propriei sale valori!</div> |
− | + | Construcţii precum | |
<syntaxhighlight lang="verilog"> | <syntaxhighlight lang="verilog"> | ||
− | out=inc? out + 1: out; | + | out = inc ? out + 1 : out; |
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | sau | |
<syntaxhighlight lang="verilog"> | <syntaxhighlight lang="verilog"> | ||
− | out <= inc? out + 1: out; | + | out <= inc ? out + 1 : out; |
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | nu pot fi folosite într-un bloc de descriere combinaţională, fie ''assign'', fie ''always @(*)''. | |
− | + | Bucla poate fi închisă numai peste un circuit combinaţional şi un registru, obţinându-se un circuit secvenţial. Prin urmare vom transforma blocul always dintr-unul combinaţional (sensibil la orice modificare a variabilelor din expresiile din partea dreapta) într-unul secvenţial, sensibil numai la fronturile active ale semnalului de ceas (obs. 1). Semnalul de ceas trebuie declarat ca intrare (obs. 2). Şi, foarte important, atribuirea din blocul secvenţial este obligatoriu nonblocantă/non-blocking (obs. 3), regulă de bază în descrierea circuitelor secvenţiale (vezi exemplul 2 din seminarul 3): | |
<syntaxhighlight lang="verilog"> | <syntaxhighlight lang="verilog"> | ||
− | module counter ( | + | module counter( |
− | input clk, //<- obs. 2 | + | input clk, // <-- obs. 2 |
input inc, | input inc, | ||
− | output reg [3: 0] out | + | output reg [3:0] out |
− | ) | + | ); |
− | always @ (posedge clk) //<- obs. 1 | + | always @(posedge clk) // <-- obs. 1 |
− | out <= inc? out + 1: out; //<- obs. 3 | + | out <= inc ? out + 1 : out; // <-- obs. 3 |
endmodule | endmodule | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | Descrierea de mai sus este echivalentă următoarei scheme bloc: | |
− | [[ | + | [[Fișier:sem5_fig1.png]] |
− | '' | + | ''Scrieţi un modul de testare a modulului counter care să genereze semnalul periodic de ceas şi semnalul de comandă, iniţial inactiv, apoi activ, şi simulaţi circuitul. Ce se observă la ieşirea lui?'' |
− | + | Descrierea de mai sus este sintetizabilă şi numărătorul sintetizat funcţionează ireproşabil. Şi totuşi la simulare ieşirea este tot timpul nedefinită. Motivul îl reprezintă valoarea iniţială a acesteia. Orice actualizare a variabilei out se bazează pe valoarea ei precedentă, iar prima execuţie a instrucţiunii de atribuire (''out <= inc ? out + 1 : out;'') ia în calcul valoarea iniţială. Valoare care este nedefinită, adică x! Adunând 1 la o valoare nedefinită se obţine tot o valoare nedefinită. Simularea este totuşi doar o aproximare a realităţii şi trebuie să fim conştienţi de limitele acesteia. | |
− | + | Un artificiu pentru a iniţializa variabila out ar fi forţarea valorii ei din afara modulului, din modulul de test. | |
− | + | În timpul verificării putem avea acces direct la orice semnal (variabilă) din interiorul modulului testat dacă folosim numele complet al acestuia, nume unic în cadrul ierarhiei de blocuri din structura modulului testat. Numele complet se specifică asemenea numelui unei variabile dintr-o ierarhie de obiecte dintr-un program scris într-un limbaj orientat pe obiecte: | |
<syntaxhighlight lang="verilog"> | <syntaxhighlight lang="verilog"> | ||
Linia 81: | Linia 81: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | În cazul numărătorului nostru, dacă în modulul de test acesta este instanţiat cu numele uut, variabila out poate fi iniţializată în pasul 0 al simulării direct din modulul de test: | |
<syntaxhighlight lang="verilog"> | <syntaxhighlight lang="verilog"> | ||
− | initial uut.out=0; | + | initial uut.out = 0; |
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | Acest mod de acces trebuie folosit cu parcimonie şi numai în module de test, nefiind o instrucţiune sintetizabilă! Se recomandă folosirea accesului direct doar în scopul citirii (monitorizării) unor variabile la care nu avem acces direct prin interfaţa instanţei de top. Atribuirile prin acces direct trebuie limitate strict la situaţiile în care ştim foarte clar toate efectele pe care le produce. | |
− | + | Reţineţi: | |
− | <div class=" | + | <div class="regula">Este interzisă în codul unui modul de circuit folosirea accesului direct la un semnal intern al altui modul.</div> |
− | + | Mult mai elegant este să proiectăm numărătorul cu o intrare de reset sau de ştergere (clear), intrare utilă în practică oricând dorim să aducem imediat numărătorul la zero. Iniţializarea numărătorului va fi făcută prin activarea intrării de ştergere. | |
− | == | + | == Exemplul 2 == |
− | '' | + | ''Adăugaţi numărătorului o intrare de resetare sincronă (numită deseori clear) activă în 0. Verificaţi circuitul folosind acelaşi modul de test în care la începutul simulării să activaţi pentru cel puţin o perioadă de ceas intrarea de resetare.'' |
− | + | Intrarea de activare a numărării (intrarea ''inc'' în exemplul 1) este de obicei numită ''enable'' sau count enable (''ce'', ''cen'' etc). | |
− | [[ | + | [[Fișier:sem5_fig2.png]] |
− | + | Pentru a verifica întreaga secvenţă de numărare trebuie să lăsăm simularea să "curgă" mai mult de 16 perioade de ceas din momentul în care se activează comanda de incrementare (''enable''). Putem să folosim în modulul de test o întârziere calculată pe baza perioadei semnalului de ceas, de exemplu ''#170'' dacă perioada ceasului este 10, sau, mai comod, putem aştepta un număr precis de fronturi active ale ceasului folosind instrucţiunea ''repeat'': | |
<syntaxhighlight lang="verilog"> | <syntaxhighlight lang="verilog"> | ||
− | repeat (17) @ (posedge clk); | + | repeat (17) @(posedge clk); |
</syntaxhighlight> | </syntaxhighlight> | ||
− | '' repeat (n) '' | + | ''repeat(n)'' repetă de n ori execuţia blocului de instrucţiuni ataşat. În cazul de mai sus se repetă de 16 ori aşteptarea frontului activ al ceasului. Avantajul acestei instrucţiuni este că nu depindem de valoarea absolută a perioadei de ceas. |
− | + | Observaţi cum numărătorul "se dă peste cap" după ce ajunge la valoarea maximă (15), reluând numărarea de la zero (se incrementează modulo 16). | |
− | == | + | == Exemplul 3 == |
− | '' | + | ''Să se descrie comportamental un numărător sincron pe 4 biţi cu încărcare sincronă, cu o intrare de activare a numărării şi o intrare de ştergere. Intrările de comandă a încărcării (''load'') şi de ştergere (''clear'') sunt active în zero. Toate comenzile acţionează numai sincron cu ceasul. Dacă sunt active simultan mai multe intrări de comandă, intrarea cea mai prioritară determină valoarea următoare a ieşirii. În ordinea descrescătoare a priorităţilor comenzile sunt: ''clear, load, enable. |
− | [[ | + | [[Fișier:sem5_fig3.png]] |
− | + | Intrarea de încărcare este activă în 0. Când aceasta este activă, frontul activ al ceasului determină încărcarea numărătorului cu valoarea de pe intrarea target. Toate funcţiile acestui numărător pot fi aranjate într-un tabel: | |
{| class="wikitable" | {| class="wikitable" | ||
|- bgcolor="#ddeeff" align="left" | |- bgcolor="#ddeeff" align="left" | ||
− | |clear || load || enable || cnt + || | + | |clear || load || enable ||cnt+ || funcţia |
|- bgcolor="#ddffdd" align="left" | |- bgcolor="#ddffdd" align="left" | ||
− | |0 || X || X || 0 || | + | |0 ||X ||X ||0 ||ştergere |
|- bgcolor="#ddffdd" align="left" | |- bgcolor="#ddffdd" align="left" | ||
− | |1 || 0 || X || target || | + | |1 ||0 ||X ||target ||încărcare |
|- bgcolor="#ddffdd" align="left" | |- bgcolor="#ddffdd" align="left" | ||
− | |1 || 1 || 0 || cnt || | + | |1 ||1 ||0 ||cnt ||memorare |
|- bgcolor="#ddffdd" align="left" | |- bgcolor="#ddffdd" align="left" | ||
− | 1 || 1 || 1 || cnt + 1 || | + | |1 ||1 ||1 ||cnt+1 ||numărare |
|} | |} | ||
− | + | În tabelul de mai sus valoarea X din dreptul unor intrări semnifică faptul că valoarea logică a acelor intrări nu contează (''don't care''). Tabelul de adevăr poate fi astfel comprimat substanţial. De exemplu, atunci când intrarea de resetare este inactivă (''clear == 1'') şi cea de încărcare e activată (''load == 0'') numărătorul încarcă (valoarea ''target'') indiferent de valoarea logică a intrării enable. Cele două combinaţii de valori de pe intrările de comandă, 100 şi 101 sunt colapsate într-una singură, 10X. | |
− | + | Această compactare poate fi folosită şi în construcţiile tabelare din Verilog, precum instrucţiunea ''case''. Pentru a putea folosi valoarea X în specificarea cazurilor instrucţiunii trebuie utilizată instrucţiunea alternativă ''casex'', care are acelaşi format ca instrucţiunea ''case'' (vezi exemplul 4 din seminarul 2): | |
<syntaxhighlight lang="verilog"> | <syntaxhighlight lang="verilog"> | ||
− | casex ( | + | casex (expresie_case) |
− | + | valoare1: instrucţiune1 | |
... | ... | ||
− | default: | + | default: instrucţiune |
endcase | endcase | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | Spre deosebire de construcţia ''case'', care admite doar valori numerice precise pentru fiecare caz, construcţia ''casex'' permite ca unii biţi să fie nedefiniţi (ignoraţi) în unele cazuri. De exemplu cazul analizat anterior poate fi descris în Verilog astfel: | |
<syntaxhighlight lang="verilog"> | <syntaxhighlight lang="verilog"> | ||
Linia 150: | Linia 150: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | '' | + | ''Verificaţi funcţionalitatea circuitului pentru toate combinaţiile de valori ale semnalelor de control.'' |
− | + | Generarea exhaustivă a tuturor combinaţiilor de valori ale semnalelor de control se face cel mai comod într-un ciclu de tip ''for'', în care tratăm combinaţia de valori ca un număr în binar pe care îl iniţializăm cu zero la începutul secvenţei de test şi pe care apoi îl incrementăm la fiecare iteraţie. Instrucţiunea ''for'' are în Verilog acelaşi format ca în C sau în Java: | |
<syntaxhighlight lang="verilog"> | <syntaxhighlight lang="verilog"> | ||
− | for ( | + | for(iniţializări; condiţie de oprire; atribuiri la sfârşit de iteraţie) |
− | + | bloc de instrucţiuni | |
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | ca de exemplu | |
<syntaxhighlight lang="verilog"> | <syntaxhighlight lang="verilog"> | ||
− | for (index=0; index <8; index=index + 1) | + | for (index = 0; index < 8; index = index + 1) |
begin | begin | ||
− | {clear, load, enable}=index; | + | {clear, load, enable} = index; |
− | repeat (16) @ (posedge clk); | + | repeat (16) @(posedge clk); |
end | end | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | Controlul buclei ''for'' îl putem face cu un index întreg care se incrementează iteraţie cu iteraţie până la valoarea dorită. | |
− | + | În modulul de test putem declara şi folosi variabile de tip întreg (''integer'') la fel ca în limbajele de programare, însă aceste variabile nu sunt semnale: | |
<syntaxhighlight lang="verilog"> | <syntaxhighlight lang="verilog"> | ||
Linia 177: | Linia 177: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | O iteraţie din bucla ''for'' de mai sus durează 16 perioade de ceas pentru fiecare combinaţie de valori testate. Observaţi modul compact în care se atribuie valori celor 3 semnale. În loc de atribuiri individuale, cele trei semnale au fost grupate într-un vector de 3 biţi căruia i se atribuie apoi o valoare întreagă. Ultimii trei biţi din reprezentarea binară a valorii variabilei ''index'' vor fi atribuiţi celor trei semnale. Gruparea unor semnale în Verilog este posibilă graţie operatorului de concatenare: | |
<syntaxhighlight lang="verilog"> | <syntaxhighlight lang="verilog"> | ||
Linia 183: | Linia 183: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | Numărul de biţi ai noii variabile este suma biţilor variabilelor grupate, iar biţii sunt dispuşi în ordinea concatenării variabilelor. În cazul nostru ''clear'' este bitul cel semnificativ (MSB) al vectorului semnalelor de control. | |
− | == | + | == Exemplul 4 == |
− | '' | + | ''Cu ajutorul numărătorului precedent, realizaţi un divizor controlabil de frecvenţă, factorul de divizare fiind selectabil astfel încât frecvenţa de ieşire să fie 1/2, 1/4, 1/8 sau 1/16 din frecvenţa ceasului.'' |
− | + | La instanţierea modulului numărător aveţi grijă ca toate intrările acestuia să fie conectate, fie la un semnal generat în modului de test, fie la masă (la valoarea 0), fie la alimentare (la valoarea 1). Pot fi lăsate neconectate (lăsate "în gol" sau "flotante") doar acele intrări care nu afectează în vreun fel funcţionarea circuitului. În exemplul nostru intrarea ''target'' poate fi neglijată deoarece numărătorul nu va încărca niciodată. În general însă, intrările neconectate pot duce la rezultate eronate datorită valorii nedefinite (X) a acestora. | |
− | + | O regulă care vă scuteşte de simulări şi sinteze surprinzătoare: | |
− | <div class=" | + | <div class="regula">Nu lăsaţi intrări neconectate!</div> |
− | == | + | == TEMA == |
− | '' | + | ''Implementaţi un generator de pulsuri cu frecvenţă şi factor de umplere controlabile. Perioada semnalului generat va fi egală cu un multiplu al perioadei ceasului, Tw = Tclk * tw, iar durata pulsului este controlată de o altă intrare, Tp = Tclk * tp.'' |
− | # | + | # folosind numărătorul din exemplul 3; |
− | # | + | # folosind o descriere pur comportamentală; |
− | [[ | + | [[Fișier:sem5_fig4.png]] |
Versiunea curentă din 27 aprilie 2018 20:52
Numărătorul (counter) este unul din cele mai utilizate circuite digitale şi sigur unul dintre cele mai cunoscute în afara lumii electroniştilor. Orice microprocesor are un numărător de program (program counter) fără de care nu poate executa programele din calculator. Numărătorul de program generează o secvenţă de adrese de memorie în ordine crescătoare, câte una în fiecare perioadă de ceas, şi din când în când, când se execută un salt în program, sare la o adresă diferită de cea imediat următoare.
Exemplul 1
Cel mai simplu numărător este registrul cu incrementare. Exercitiul 4 din seminarul 2 v-a cerut descrierea unui circuit combinaţional de incrementare comandată, una din variantele posibile fiind:
module increment(
input inc,
input [3:0] a,
output reg [3:0] out
);
always @(*)
out = inc ? a + 1 : a;
endmodule
Cu minime modificări descrierea de mai sus poate fi transformată într-un numărător cu incrementare comandată. Pentru a genera o secvenţă crescătoare de numere, numărul de la ieşire este readus la intrare. Bucla o putem închide în interiorul modulului, renunţând la intrarea a:
module incorect(
input inc,
output reg [3:0] out
);
always @(*)
out = inc ? out + 1 : out; // buclă infinită!
endmodule
Închiderea unei bucle peste un circuit combinaţional duce de cele mai multe ori la un circuit instabil, iar în cazul descrierii de mai sus simularea va intra într-o buclă infinită. Reţineţi aşadar o regulă de proiectare:
Construcţii precum
out = inc ? out + 1 : out;
sau
out <= inc ? out + 1 : out;
nu pot fi folosite într-un bloc de descriere combinaţională, fie assign, fie always @(*).
Bucla poate fi închisă numai peste un circuit combinaţional şi un registru, obţinându-se un circuit secvenţial. Prin urmare vom transforma blocul always dintr-unul combinaţional (sensibil la orice modificare a variabilelor din expresiile din partea dreapta) într-unul secvenţial, sensibil numai la fronturile active ale semnalului de ceas (obs. 1). Semnalul de ceas trebuie declarat ca intrare (obs. 2). Şi, foarte important, atribuirea din blocul secvenţial este obligatoriu nonblocantă/non-blocking (obs. 3), regulă de bază în descrierea circuitelor secvenţiale (vezi exemplul 2 din seminarul 3):
module counter(
input clk, // <-- obs. 2
input inc,
output reg [3:0] out
);
always @(posedge clk) // <-- obs. 1
out <= inc ? out + 1 : out; // <-- obs. 3
endmodule
Descrierea de mai sus este echivalentă următoarei scheme bloc:
Scrieţi un modul de testare a modulului counter care să genereze semnalul periodic de ceas şi semnalul de comandă, iniţial inactiv, apoi activ, şi simulaţi circuitul. Ce se observă la ieşirea lui?
Descrierea de mai sus este sintetizabilă şi numărătorul sintetizat funcţionează ireproşabil. Şi totuşi la simulare ieşirea este tot timpul nedefinită. Motivul îl reprezintă valoarea iniţială a acesteia. Orice actualizare a variabilei out se bazează pe valoarea ei precedentă, iar prima execuţie a instrucţiunii de atribuire (out <= inc ? out + 1 : out;) ia în calcul valoarea iniţială. Valoare care este nedefinită, adică x! Adunând 1 la o valoare nedefinită se obţine tot o valoare nedefinită. Simularea este totuşi doar o aproximare a realităţii şi trebuie să fim conştienţi de limitele acesteia.
Un artificiu pentru a iniţializa variabila out ar fi forţarea valorii ei din afara modulului, din modulul de test.
În timpul verificării putem avea acces direct la orice semnal (variabilă) din interiorul modulului testat dacă folosim numele complet al acestuia, nume unic în cadrul ierarhiei de blocuri din structura modulului testat. Numele complet se specifică asemenea numelui unei variabile dintr-o ierarhie de obiecte dintr-un program scris într-un limbaj orientat pe obiecte:
numeInstanţăTop.semnal
numeInstanţăTop.numeInstanţăBloc.semnal
numeInstanţăTop.numeInstanţăBloc.numeInstanţăSubbloc.semnal
În cazul numărătorului nostru, dacă în modulul de test acesta este instanţiat cu numele uut, variabila out poate fi iniţializată în pasul 0 al simulării direct din modulul de test:
initial uut.out = 0;
Acest mod de acces trebuie folosit cu parcimonie şi numai în module de test, nefiind o instrucţiune sintetizabilă! Se recomandă folosirea accesului direct doar în scopul citirii (monitorizării) unor variabile la care nu avem acces direct prin interfaţa instanţei de top. Atribuirile prin acces direct trebuie limitate strict la situaţiile în care ştim foarte clar toate efectele pe care le produce. Reţineţi:
Mult mai elegant este să proiectăm numărătorul cu o intrare de reset sau de ştergere (clear), intrare utilă în practică oricând dorim să aducem imediat numărătorul la zero. Iniţializarea numărătorului va fi făcută prin activarea intrării de ştergere.
Exemplul 2
Adăugaţi numărătorului o intrare de resetare sincronă (numită deseori clear) activă în 0. Verificaţi circuitul folosind acelaşi modul de test în care la începutul simulării să activaţi pentru cel puţin o perioadă de ceas intrarea de resetare.
Intrarea de activare a numărării (intrarea inc în exemplul 1) este de obicei numită enable sau count enable (ce, cen etc).
Pentru a verifica întreaga secvenţă de numărare trebuie să lăsăm simularea să "curgă" mai mult de 16 perioade de ceas din momentul în care se activează comanda de incrementare (enable). Putem să folosim în modulul de test o întârziere calculată pe baza perioadei semnalului de ceas, de exemplu #170 dacă perioada ceasului este 10, sau, mai comod, putem aştepta un număr precis de fronturi active ale ceasului folosind instrucţiunea repeat:
repeat (17) @(posedge clk);
repeat(n) repetă de n ori execuţia blocului de instrucţiuni ataşat. În cazul de mai sus se repetă de 16 ori aşteptarea frontului activ al ceasului. Avantajul acestei instrucţiuni este că nu depindem de valoarea absolută a perioadei de ceas.
Observaţi cum numărătorul "se dă peste cap" după ce ajunge la valoarea maximă (15), reluând numărarea de la zero (se incrementează modulo 16).
Exemplul 3
Să se descrie comportamental un numărător sincron pe 4 biţi cu încărcare sincronă, cu o intrare de activare a numărării şi o intrare de ştergere. Intrările de comandă a încărcării (load) şi de ştergere (clear) sunt active în zero. Toate comenzile acţionează numai sincron cu ceasul. Dacă sunt active simultan mai multe intrări de comandă, intrarea cea mai prioritară determină valoarea următoare a ieşirii. În ordinea descrescătoare a priorităţilor comenzile sunt: clear, load, enable.
Intrarea de încărcare este activă în 0. Când aceasta este activă, frontul activ al ceasului determină încărcarea numărătorului cu valoarea de pe intrarea target. Toate funcţiile acestui numărător pot fi aranjate într-un tabel:
clear | load | enable | cnt+ | funcţia |
0 | X | X | 0 | ştergere |
1 | 0 | X | target | încărcare |
1 | 1 | 0 | cnt | memorare |
1 | 1 | 1 | cnt+1 | numărare |
În tabelul de mai sus valoarea X din dreptul unor intrări semnifică faptul că valoarea logică a acelor intrări nu contează (don't care). Tabelul de adevăr poate fi astfel comprimat substanţial. De exemplu, atunci când intrarea de resetare este inactivă (clear == 1) şi cea de încărcare e activată (load == 0) numărătorul încarcă (valoarea target) indiferent de valoarea logică a intrării enable. Cele două combinaţii de valori de pe intrările de comandă, 100 şi 101 sunt colapsate într-una singură, 10X. Această compactare poate fi folosită şi în construcţiile tabelare din Verilog, precum instrucţiunea case. Pentru a putea folosi valoarea X în specificarea cazurilor instrucţiunii trebuie utilizată instrucţiunea alternativă casex, care are acelaşi format ca instrucţiunea case (vezi exemplul 4 din seminarul 2):
casex (expresie_case)
valoare1: instrucţiune1
...
default: instrucţiune
endcase
Spre deosebire de construcţia case, care admite doar valori numerice precise pentru fiecare caz, construcţia casex permite ca unii biţi să fie nedefiniţi (ignoraţi) în unele cazuri. De exemplu cazul analizat anterior poate fi descris în Verilog astfel:
3'b10X: out <= target;
Verificaţi funcţionalitatea circuitului pentru toate combinaţiile de valori ale semnalelor de control.
Generarea exhaustivă a tuturor combinaţiilor de valori ale semnalelor de control se face cel mai comod într-un ciclu de tip for, în care tratăm combinaţia de valori ca un număr în binar pe care îl iniţializăm cu zero la începutul secvenţei de test şi pe care apoi îl incrementăm la fiecare iteraţie. Instrucţiunea for are în Verilog acelaşi format ca în C sau în Java:
for(iniţializări; condiţie de oprire; atribuiri la sfârşit de iteraţie)
bloc de instrucţiuni
ca de exemplu
for (index = 0; index < 8; index = index + 1)
begin
{clear, load, enable} = index;
repeat (16) @(posedge clk);
end
Controlul buclei for îl putem face cu un index întreg care se incrementează iteraţie cu iteraţie până la valoarea dorită. În modulul de test putem declara şi folosi variabile de tip întreg (integer) la fel ca în limbajele de programare, însă aceste variabile nu sunt semnale:
integer index;
O iteraţie din bucla for de mai sus durează 16 perioade de ceas pentru fiecare combinaţie de valori testate. Observaţi modul compact în care se atribuie valori celor 3 semnale. În loc de atribuiri individuale, cele trei semnale au fost grupate într-un vector de 3 biţi căruia i se atribuie apoi o valoare întreagă. Ultimii trei biţi din reprezentarea binară a valorii variabilei index vor fi atribuiţi celor trei semnale. Gruparea unor semnale în Verilog este posibilă graţie operatorului de concatenare:
{clear, load, enable}
Numărul de biţi ai noii variabile este suma biţilor variabilelor grupate, iar biţii sunt dispuşi în ordinea concatenării variabilelor. În cazul nostru clear este bitul cel semnificativ (MSB) al vectorului semnalelor de control.
Exemplul 4
Cu ajutorul numărătorului precedent, realizaţi un divizor controlabil de frecvenţă, factorul de divizare fiind selectabil astfel încât frecvenţa de ieşire să fie 1/2, 1/4, 1/8 sau 1/16 din frecvenţa ceasului.
La instanţierea modulului numărător aveţi grijă ca toate intrările acestuia să fie conectate, fie la un semnal generat în modului de test, fie la masă (la valoarea 0), fie la alimentare (la valoarea 1). Pot fi lăsate neconectate (lăsate "în gol" sau "flotante") doar acele intrări care nu afectează în vreun fel funcţionarea circuitului. În exemplul nostru intrarea target poate fi neglijată deoarece numărătorul nu va încărca niciodată. În general însă, intrările neconectate pot duce la rezultate eronate datorită valorii nedefinite (X) a acestora. O regulă care vă scuteşte de simulări şi sinteze surprinzătoare:
TEMA
Implementaţi un generator de pulsuri cu frecvenţă şi factor de umplere controlabile. Perioada semnalului generat va fi egală cu un multiplu al perioadei ceasului, Tw = Tclk * tw, iar durata pulsului este controlată de o altă intrare, Tp = Tclk * tp.
- folosind numărătorul din exemplul 3;
- folosind o descriere pur comportamentală;