I due operatori
Dentro i blocchi always e initial, Verilog ha due operatori di assegnazione:
=è un'assegnazione blocking (bloccante): aggiorna subito il lato sinistro, prima di passare all'istruzione successiva.<=è un'assegnazione non-blocking (non bloccante): valuta subito il lato destro e programma l'aggiornamento del lato sinistro alla fine del passo temporale corrente.
Fuori dai blocchi procedurali (in assign) esiste solo =: assign a <= b è un errore di sintassi. Dentro i blocchi procedurali sono validi entrambi, e scegliere quello giusto è la decisione più importante per chi inizia con Verilog.
Cosa fa l'assegnazione blocking
L'assegnazione blocking si comporta come ti aspetteresti da un linguaggio software: riga per riga, dall'alto in basso. L'istruzione N termina prima che inizi l'istruzione N+1:
È puramente sequenziale. Ogni istruzione vede gli effetti di quelle precedenti, proprio come suggerisce l'intuizione del software.
Cosa fa l'assegnazione non-blocking
L'assegnazione non-blocking ragiona come l'hardware. Tutti i lati destri vengono valutati con i valori all'inizio del passo temporale. Tutti i lati sinistri si aggiornano alla fine del passo. L'ordine delle istruzioni nel sorgente non cambia le dipendenze tra i segnali:
Questo frammento implementa in un solo passo una rotazione a tre a → b → c → a. Con l'assegnazione blocking ti servirebbe una variabile temporanea per non sovrascrivere uno dei valori. Con quella non-blocking lo scambio avviene in modo atomico, perché ogni lato destro legge i valori precedenti al passo.
È esattamente il comportamento di tre flip-flop su un fronte di clock: catturano tutti i propri ingressi nello stesso istante, indipendentemente dalle dipendenze tra loro.
La regola che ti salva
Usa <= nei blocchi sincronizzati. Usa = nei blocchi combinatori.
Tutto qui. Memorizzala. Applicala senza pensarci. La maggior parte delle race condition, delle differenze tra simulazione e sintesi e dei bug del tipo "da me funziona ma in sintesi no" nasce dalla violazione di questa regola.
La regola ha una ragione hardware: i blocchi sincronizzati modellano flip-flop che campionano tutti insieme i propri ingressi, mentre i blocchi combinatori modellano logica che si propaga il più velocemente possibile. La semantica degli operatori di assegnazione rispecchia questi due comportamenti.
Ogni <= legge il valore attuale del registro sorgente e programma il registro di destinazione perché assuma quel valore alla fine del passo temporale. L'effetto complessivo è esattamente quello di uno shift register hardware: ogni flip-flop cattura il valore del vicino, tutti sullo stesso fronte di clock, senza race.
Ora guarda cosa sarebbe successo con l'assegnazione blocking:
// WRONG - this is not a shift register!
always @(posedge clk) begin
out[3] = out[2]; // out[3] becomes out[2]
out[2] = out[1]; // out[2] becomes out[1], which we just set above
out[1] = out[0];
out[0] = in;
end
Ogni istruzione sovrascrive la sorgente prima che l'istruzione successiva la legga. In un solo ciclo di clock, in si propagherebbe fino a out[3], perché ogni riga vede il valore appena scritto dalla riga precedente. Il comportamento dell'hardware reale (che usa la semantica non-blocking) sarebbe completamente diverso da quello mostrato dal simulatore.
Blocchi combinatori: blocking è la scelta giusta
Per always @(*) l'assegnazione blocking è corretta. Non ci sono flip-flop né una regola di cattura simultanea da rispettare, e le variabili intermedie tornano utili:
Prima viene calcolato sum, poi result usa il valore appena calcolato. La logica combinatoria si appiattisce in un unico pezzo di hardware: result = ~(a + b). Non compaiono flip-flop perché non c'è clock.
Se qui usassi <=, il simulatore aggiornerebbe comunque sum prima di valutarci sopra result (perché entrambi gli aggiornamenti avvengono a fine passo), ma l'ordine sarebbe sottilmente diverso e molti strumenti di sintesi protestano. Non mescolarli: scegli l'operatore che corrisponde al tipo di blocco.
L'errore che fa più male
Eccolo: un blocco sincronizzato con assegnazione blocking.
// BUG: race condition waiting to happen
always @(posedge clk) begin
a = b;
b = c;
c = a;
end
In simulazione, il simulatore potrebbe assegnare prima a, poi b, poi c, producendo un certo insieme di valori. L'hardware ne produrrà un altro, perché i flip-flop reali catturano simultaneamente. I due risultati divergono in silenzio e perderai una giornata a trovare il bug. Usa <= nei blocchi sincronizzati.
Perché esistono due operatori
I progettisti di Verilog avrebbero potuto scegliere una sola semantica di assegnazione. Non l'hanno fatto perché il linguaggio deve modellare due comportamenti hardware distinti:
- Logica combinatoria: i segnali si propagano di continuo, le dipendenze contano e chiedersi "cosa calcola questa porta" ha senso.
- Logica sequenziale: arriva un fronte di clock, ogni flip-flop cattura simultaneamente e le dipendenze tra ingressi e uscite dei flip-flop sono disaccoppiate.
L'assegnazione blocking serve per il primo caso, quella non-blocking per il secondo. L'operatore sceglie la semantica; al resto pensa il simulatore.
Cosa viene dopo
Ora hai le regole per scrivere correttamente qualsiasi blocco procedurale. Il prossimo capitolo passa dai singoli blocchi ai costrutti di controllo del flusso che vanno al loro interno: if/else, case e i cicli for. Le regole su blocking e non-blocking valgono anche dentro tutti questi costrutti.
Domande frequenti
Qual è la differenza tra assegnazione blocking e non-blocking in Verilog?
L'assegnazione blocking (=) aggiorna subito la destinazione, prima che venga eseguita l'istruzione successiva, e modella un'esecuzione sequenziale. L'assegnazione non-blocking (<=) programma l'aggiornamento alla fine del passo temporale corrente: ogni lato destro non-blocking viene valutato con i valori vecchi di tutti i segnali, poi tutti i lati sinistri si aggiornano in un unico passo coordinato. È così che si comportano davvero i flip-flop.
Quando usare = e quando <= in Verilog?
Regola: usa <= nei blocchi sincronizzati always @(posedge clk) e = nei blocchi combinatori always @(*). Questa sola regola elimina l'intera categoria di race condition prodotte dalle assegnazioni miste. Nei blocchi initial dei testbench la scelta normale è =; lì <= compare di rado.
Perché in Verilog serve l'assegnazione non-blocking?
Perché i flip-flop hardware catturano tutti insieme i propri ingressi sul fronte di clock. Se usassi = (blocking) nel codice sincronizzato, l'ordine delle istruzioni nel file cambierebbe quale segnale vede il nuovo valore di quale altro: una race condition tra simulazione e hardware reale. <= riproduce il comportamento dell'hardware valutando prima tutti i lati destri e poi eseguendo tutti gli aggiornamenti.
Cosa succede se mescoli = e <= nello stesso blocco always in Verilog?
Ottieni una race condition. Il miscuglio produce hardware il cui comportamento dipende dai meccanismi interni del simulatore (l'ordine di scheduling degli eventi) e, peggio ancora, la simulazione può non corrispondere a ciò che produce la sintesi. La maggior parte dei linter lo segnala come errore. La soluzione è scegliere uno dei due in base al ruolo del blocco: non-blocking per i blocchi sincronizzati, blocking per quelli combinatori.