Sintassi familiare, modello mentale diverso
if/else ha esattamente l'aspetto del C:
if (condition) begin
// ... statements ...
end else begin
// ... statements ...
end
Le regole però sono diverse, perché Verilog non è software. Due cose da tenere a mente:
if/elsevive solo dentro un blocco procedurale. Non puoi scrivere unifisolato al livello del modulo.- Ciò in cui si sintetizza
if/elsedipende dal tipo di blocco. In un blocco combinatorio diventa un multiplexer o un priority encoder. In un blocco sincronizzato dal clock diventa un flip-flop con logica di aggiornamento condizionale.
Dentro un blocco combinatorio
Il blocco combinatorio always @(*) si riesegue ogni volta che cambia a, b o c. La catena if/else sceglie un ramo, assegna max e il blocco termina. Poiché max viene sempre assegnato (ogni percorso contiene un'assegnazione), il sintetizzatore produce pura logica combinatoria: nessun latch.
Nota che max è dichiarato reg anche se nell'hardware non c'è nessun flip-flop. Solita regola: tutto ciò che viene assegnato dentro always deve essere reg.
La trappola del latch
È il bug in assoluto più comune nel codice combinatorio di chi inizia:
// WRONG - infers a latch
always @(*) begin
if (enable)
out = data;
// no else! when `enable` is low, what does `out` do?
end
Il sintetizzatore legge "quando enable è basso, out non viene assegnato" e conclude che out deve ricordare il valore precedente. Ricordare un valore richiede una cella di memoria, quindi lo strumento inserisce un latch. Nei progetti sincroni i latch causano problemi di temporizzazione, sono difficili da resettare e non sono quasi mai ciò che intendevi.
Due modi per correggerlo:
Entrambi producono lo stesso hardware combinatorio: un mux 2 a 1. Il pattern "valore predefinito in cima" scala meglio quando hai molte assegnazioni condizionali allo stesso segnale.
Dentro un blocco sincronizzato
Nota cosa cambia rispetto al caso combinatorio:
- Il blocco è
always @(posedge clk): territorio dei flip-flop. - L'assegnazione usa
<=(non-blocking). - Non c'è un
elseper il caso "né reset né enable". Va bene così. In un blocco sincronizzato, quando nessun ramo scatta, il flip-flop mantiene semplicemente il valore precedente, che è proprio ciò che un flip-flop fa fisicamente. Nessun latch viene dedotto, perché il segnale è già un registro.
È l'unico caso in cui omettere un else è sicuro. Fuori dai blocchi sincronizzati, gestisci sempre ogni percorso.
Catene di else if: un priority encoder
Una catena di istruzioni else if ha una priorità implicita: le condizioni precedenti prevalgono su quelle successive:
requests[0] ha la priorità più alta: se è attivo, il grant vale 0 qualunque cosa facciano i bit di indice maggiore. Il sintetizzatore trasforma la catena in mux in cascata: controlla prima il bit 0, poi il bit 1, poi il bit 2, poi il bit 3. Ogni livello aggiunge un piccolo ritardo.
Se le condizioni sono mutuamente esclusive, per esempio nella decodifica di un ingresso one-hot, un'istruzione case (prossima pagina) produce hardware più piatto e veloce di una catena di else if. Usa la forma con case quando non c'è un vero requisito di priorità.
if senza else nel codice sincronizzato
Un blocco sincronizzato non ha bisogno di un else, perché "mantieni il valore precedente" è il comportamento predefinito. È così che si costruiscono gli enable:
always @(posedge clk) begin
if (load) target <= incoming;
// no else: when load is low, target keeps its value
end
Questo è un registro con abilitazione al caricamento. La maggior parte dei registri di pipeline, dei contatori e dei registri di configurazione usa questo pattern.
begin/end e istruzioni singole
Come in C, puoi omettere begin/end per una singola istruzione:
if (a) out = 1;
else out = 0;
Per più di un'istruzione, usa il blocco:
if (a) begin
out = 1;
flag = 1;
end else begin
out = 0;
flag = 0;
end
Le due forme si possono mescolare liberamente. In genere le guide di stile consigliano di usare sempre begin/end, così aggiungere una seconda istruzione non crea problemi.
Cosa viene dopo
La prossima pagina, Case Statement, tratta case, lo strumento giusto per la decodifica a più vie (macchine a stati, smistamento degli opcode, tabelle ROM). Poi arrivano i cicli for, sottilmente diversi dai loro cugini software perché vengono srotolati durante l'elaborazione.
Domande frequenti
Come funziona un'istruzione if in Verilog?
if (cond) statement; esegue statement quando cond è diverso da zero. Puoi racchiudere più istruzioni in begin ... end. Aggiungi else statement; per il ramo alternativo, oppure concatena con else if (other_cond) .... if/else esiste solo dentro i blocchi procedurali, initial o always, non al livello principale di un modulo.
Cos'è un latch dedotto (inferred latch) in Verilog?
È un latch che lo strumento di sintesi ha creato senza che tu lo chiedessi, perché il tuo blocco always combinatorio non assegnava un segnale in ogni percorso. Lo strumento vede 'se a allora out = 1' senza else, conclude che nel caso non assegnato il segnale deve ricordare il valore precedente e produce un latch. I latch sono quasi sempre un errore; la soluzione è dare a ogni segnale un valore predefinito in cima al blocco oppure un else esplicito.
Come si evitano i latch dedotti in Verilog?
In un blocco combinatorio always @(*), assicurati che ogni reg di uscita venga assegnato in ogni percorso del codice. Il pattern più pulito è impostare i valori predefiniti in cima al blocco e poi sovrascriverli in modo condizionale. Di solito il compilatore avvisa quando deduce un latch: tratta l'avviso come un errore.
Cosa si sintetizza da una catena if-else in Verilog?
Un priority encoder. Il primo if ha la priorità più alta, il successivo else if viene valutato solo se il primo è falso, e così via. In hardware diventa una catena di mux con l'ordine di priorità incorporato. Se le condizioni sono mutuamente esclusive, un'istruzione case con la stessa logica spesso si sintetizza in hardware più piatto e si legge meglio.