Sintassi per reti sincronizzate
Una rete sincronizzata si esprime come un module
contenente registri, che sono espressi con reg
il cui valore è inizializzato in risposta a reset_
ed aggiornato in risposta a fronti positivi del clock
.
Gran parte della sintassi già vista per le reti combinatorie rimane valida anche qui, e dunque non la ripetiamo.
Ci focalizziamo invece su come esprimere registri usando reg
.
Istanziazione
Un registro si istanzia con statement simili a quelli per wire
:
reg [3:0] R1, R2;
reg R3, R4, R5;
Verilog è case sensitive, cioè distingue come diversi nomi che differiscono solo per la capitalizzazione, come out
e OUT
.
Nel corso, utilizziamo questa feature per distinguere a colpo d'occhio reg
e wire
, utilizzando lettere maiuscole per i primi e minuscole per i secondi.
Questo è particolarmente utile quando si hanno registri a sostegno di un wire, tipicamente un'uscita della rete o l'ingresso di un module
interno.
Seguire questa convenzione non è obbligatorio, ma fortemente consigliato per evitare ambiguità ed errori che ne conseguono.
Collegamento a wire
Un reg
si può utilizzare come "fonte di valore" per un wire
.
Questo equivale circuitalmente a collegare il wire
all'uscita del reg
.
output out;
reg OUT;
assign out = OUT;
In questo caso, out
seguirà sempre e in modo continuo il valore di OUT
, propagandolo a ciò a cui viene collegato a sua volta.
In questo caso non introduciamo nessun ritardo #T
nell'assign
perché si tratta di un semplice collegamento senza logica combinatoria aggiunta.
Allo stesso modo, si può collegare un reg
all'ingresso di una rete.
reg [3:0] X, Y;
add #( .N(4) ) a(
.x(X), .y(Y), .c_in(1'b0),
...
);
Non ha invece alcun senso cercare di fare il contrario, ossia collegare direttamente un wire
all'ingresso di un reg
.
Anche se questo ha senso circuitalmente, Verilog richiede di esprimere questo all'interno di un blocco always
per indicare anche quando aggiornare il valore del reg
.
Struttura generale di un blocco always
Il valore di un reg
si aggiorna all'interno di blocchi always
.
La sintassi generale di questi blocchi è la seguente
always @( event ) [if( cond )] [ #T ] begin
[multiple statements]
end
Il funzionamento è il seguente: ogni volta che accade event
, se cond
è vero e dopo tempo T
, vengono eseguiti gli statement indicati.
Se lo statement è uno solo, si possono anche omettere begin
e end
.
Per Verilog, qui come statement si possono usare tutte le sintassi procedurali che si desiderano, incluse quelle discusse per le testbench che permettono di scrivere un classico programma "stile C". Per noi, no. Useremo questi blocchi in dei modi specifici per indicare
- come si comportano i registri al reset,
- come si comportano i registri al fronte positivo del
clock
.
Comportamento al reset
Per indicare il comportamento al reset useremo statement del tipo
always @(reset_ == 0) begin
R1 = 0;
end
Il funzionamento è facilmente intuibile: finché reset_
è a 0, il reg
è impostato al valore indicato.
Il blocco begin ... end
può contenere l'inizializzazione di più registri.
Tipicamente, raggrupperemo tutte le inizializzazioni in una descrizione, mentre le terremo separate in una sintesi.
Un registro può non essere inizializzato: in tal caso, il suo valore sarà non specificato, in Verilog X
.
Ricordiamo che questo significa che il registro ha un qualche valore misurabile, ma non è possibile determinare logicamente a priori e in modo univoco quale sarà.
In un blocco reset è indifferente l'uso di =
o <=
per gli assegnamenti (vedere sezione più avanti).
Per la sintassi Verilog, a destra dell'assegnamento si potrebbe utilizzare qualunque espressione, sia questa costante (per esempio, il letterale 1'b0
o un parameter
) o variabile (per esempio, il wire w
).
Se pensiamo però all'equivalente circuitale, hanno senso solo valori costanti.
Infatti, impostare un valore al reset equivale a collegare opportunamente i piedini preset_
e preclear_
del registro.
Aggiornamento al fronte positivo del clock
Per indicare il comportamento al fronte positivo del clock
useremo statement del tipo
always @(posedge clock) if(reset_ == 1) #3 begin
OUT <= ~OUT;
end
Il funzionamento è il seguente: ad ogni fronte positivo del clock
, se reset_
è a 1 e dopo 3 unità di tempo, il registro viene aggiornato con il valore indicato.
Differentemente dal reset, qui si può utilizzare qualunque logica combinatoria per il calcolo del nuovo valore del registro.
L'unità di tempo (impostato a 3 in questo corso solo per convenzione, così come il periodo del clock a 10 unità) rappresenta il tempo di propagazione del registro, ossia il tempo che passa dal fronte del clock prima che il registro mostri in uscita il nuovo valore.
Tutti gli assegmenti in questi blocchi devono usare l'operatore <=
, e non =
.
Come spiegato nella sezione più avanti, questo è necessario perché i registri simulati siano non-trasparenti.
Tipicamente usiamo registri multifunzionali, ossia che operano in maniera diversa in base allo stato della rete.
In una descrizione, questo si fa usando un singolo registro di stato STAR
e indicando il comportamento dei vari registri multifunzionali al variare di STAR
.
Questo ci fa vedere in generale come si comporta l'intera rete al variare di STAR
.
In questa notazione, è lecito omettere un registro in un dato stato, implicando che quel registro conserva il valore precedentemente assegnato.
localparam S0 = 0, S1 = 1;
always @(posedge clock) if(reset_ == 1) #3 begin
casex(STAR)
S0: begin
A <= ~B;
B <= A;
STAR <= (A == 1'b0) ? S1 : S0;
end
S1: begin
A <= B;
B <= ~A;
STAR <= (B == 1'b1) ? S1 : S0;
end
endcase
end
In una sintesi, invece, si sintetizza ciascun registro individualmente come un multiplexer guidato da una serie di variabili di comando.
Il multiplexer ha come ingressi tutti i risultati combinatori che il registro utilizza, e in base allo stato (da cui vengono generate le variabili di comando) solo uno di questi è utilizzato per aggiornare il registro al fronte positivo del clock.
Questo è rappresentato in Verilog utilizzando le variabili di comando per discriminare il casex
, e indicando un comportamento combinatorio per ciascun valore di queste variabili.
In questa notazione, non è lecito omettere le operazioni di conservazione, mentre è lecito utilizzare non specificati per indicare comportamenti assegnati a più ingressi del multiplexer.
Nell'esempio sotto, con 2'b1X
si indica che a entrambi gli ingressi 10
e 11
del multiplexer è collegato il valore DAV_
.
always @(posedge clock) if(reset_ == 1) #3 begin
casex({b1, b0})
2'b00: DAV_ <= 0;
2'b01: DAV_ <= 1;
2'b1X: DAV_ <= DAV_;
endcase
end
Limitazioni della simulazione: temporizzazione, non-trasparenza e operatori di assegnamento
Ci sono alcune differenze tra i registri, intesi come componenti elettronici, e i reg
descritti in Verilog così come abbiamo visto.
Queste differenze non sono d'interesse se non si fanno errori.
In caso di errori, si potrebbero osservare comportamenti altrimenti inspiegabili, ed è per questo che è utile conoscere queste differenze per poter risalire alla fonte del problema.
I registri hanno caratteristiche di temporizzazione sia prima che dopo il fronte positivo del clock: ciascun ingresso va impostato almeno prima del fronte positivo, mantenuto fino ad almeno dopo, e il valore in ingresso è rispecchiato in uscita solo dopo .
Date le semplici strutture sintattiche che utilizziamo, la simulazione non è così accurata e non considera e . In particolare, il simulatore campiona i valori in ingresso non prima del fronte positivo, ma direttamente quando aggiorna il valore dei registri, ossia dopo dal fronte positivo del clock.
In altre parole: tutti i campionamenti e gli aggiornamenti dei registri sono fatti allo stesso tempo di simulazione, ossia dopo il fronte positivo del clock.
Questo porterebbe a violare la non-trasparenza dei registri, se non fosse per l'operatore di assegnamento <=
, detto non-blocking assignement.
Questo operatore si comporta in questo modo: tutti gli assegmenti <=
contemporanei (ossia allo stesso tempo di simulazione) non hanno effetto l'uno sull'altro perché campionano il right hand side all'inizio del time-step e aggiornano il left hand side alla fine del time-step.
Questo simula correttamente la non-trasparenza dei registri, ma solo se tutti usano <=
.
Gli assegnamenti con =
, detti blocking assignement, sono invece eseguiti completamente e nell'ordine in cui li incontra il simulatore (si assuma che quest'ordine sia del tutto casuale).
Al tempo di reset questo ci è indifferente, perché sono (circuitalmente) leciti solo assegnamenti con valori costanti e non si possono quindi creare anelli per cui è di interesse la non-trasparenza.