La diramazione a più vie
case è il costrutto di smistamento piatto di Verilog. Gli dai un'espressione e lui sceglie il ramo corrispondente:
case (expression)
pattern_1: statement_1;
pattern_2: statement_2;
pattern_3: begin
statement_3a;
statement_3b;
end
default: default_statement;
endcase
Strutturalmente somiglia allo switch del C, ma:
- Non esiste
break: ogni ramo termina implicitamente dove inizia il successivo. - I pattern possono essere vettori, non solo interi.
- Il sintetizzatore trasforma l'intero costrutto in un mux piatto (o in un decodificatore one-hot, quando i pattern lo permettono).
Un esempio completo: mux 4 a 1
Il corpo del case ha quattro pattern espliciti più un default. Il sintetizzatore vede un ingresso a 2 bit che corrisponde a uno di quattro valori e genera un multiplexer 4 a 1. Pulito, piatto, veloce.
Perché ti serve default
In un case combinatorio, omettere default è la stessa trappola di un if senza else: qualsiasi valore di ingresso non previsto lascia out senza assegnazione e il sintetizzatore deduce un latch.
Con un sel a 2 bit, i pattern qui sopra coprono tutti e quattro i valori possibili, quindi in teoria default è superfluo. In pratica:
- Il sintetizzatore non riesce sempre a dimostrare che i casi sono esaustivi.
- In simulazione il selettore può valere
xoz, che non corrisponde a nessun caso esplicito. - Aggiungere un nuovo caso in seguito potrebbe lasciare il comportamento di default non specificato.
Scrivi sempre default. Per macchine a stati e logica di mux in cui sai che il default è irraggiungibile, l'assegnazione a 'x:
default: out = 8'bx;
…dice al sintetizzatore "questo è un don't care, ottimizza liberamente" e fa comparire in simulazione un x ben visibile se in qualche modo il caso irraggiungibile viene raggiunto. Il meglio dei due mondi.
Una macchina a stati con case
L'uso classico di case è la logica di transizione tra stati di una macchina a stati finiti:
Il blocco case (state) è la logica di transizione. Ogni ramo decide qual è lo stato successivo e per quanto tempo restarci. Qui default è irraggiungibile (copriamo in modo esaustivo RED/GREEN/YELLOW nello spazio a 2 bit), ma c'è come rete di sicurezza: se state diventasse in qualche modo 2'd3, la FSM tornerebbe pulitamente a RED invece di generare un latch.
Finite State Machines approfondisce questo pattern.
Più pattern per ramo
Puoi elencare più pattern che condividono una singola istruzione, separandoli con virgole:
case (opcode)
4'h0, 4'h1, 4'h2: result = a + b;
4'h3, 4'h4: result = a - b;
4'h8: result = a & b;
default: result = 8'd0;
endcase
Qui due opcode significano entrambi "sottrai" e tre significano "somma". Il sintetizzatore mette in OR i pattern per costruire il comparatore.
casez e casex: confronto con don't care
A volte vuoi confrontare un pattern lasciando alcuni bit non specificati, per esempio "qualsiasi opcode che inizia con 010":
casez (opcode)
8'b010?_????: instruction = ALU_OP;
8'b110?_????: instruction = LOAD_OP;
8'b1110_????: instruction = JUMP_OP;
default: instruction = UNKNOWN;
endcasez
casez tratta ? (e z) nel pattern come don't care. Ogni ? corrisponde sia a 0 sia a 1. È utile per decodificare formati di istruzione in cui alcune posizioni di bit non sono usate da certe classi di opcode.
casex estende il meccanismo trattando come don't care anche x. casex è pericoloso perché i segnali non inizializzati (che in simulazione valgono x) corrispondono a ogni caso, con comportamenti sorprendenti. La maggior parte delle guide di stile moderne consiglia casez e vieta casex.
In SystemVerilog hai anche case inside, la versione più pulita di tutte (accetta intervalli ed elenchi), ma il Verilog semplice si ferma a casez.
case contro una catena di if/else if
Entrambi i costrutti possono esprimere decisioni a più vie, ma si sintetizzano in hardware diverso:
caseè uno smistamento piatto. Il sintetizzatore può produrre un decodificatore one-hot, un albero di mux bilanciato o altre strutture piatte. Il tempo di valutazione è costante.if/else ifè una catena di priorità. Il sintetizzatore produce una cascata in cui ogni livello aggiunge ritardo. È più lento in modo logaritmico.
Dal punto di vista funzionale si sovrappongono. Come stile: usa case quando le condizioni riguardano il valore di una singola espressione. Usa if/else if quando esiste una vera priorità o quando le condizioni coinvolgono segnali diversi.
// Better as case:
if (sel == 2'd0) out = a;
else if (sel == 2'd1) out = b;
else if (sel == 2'd2) out = c;
else out = d;
// Better as if/else if:
if (urgent_event) next_state = HANDLE_URGENT;
else if (timer_expired) next_state = TIMEOUT;
else if (data_ready) next_state = PROCESS;
else next_state = state;
Le prime tre condizioni si chiedono tutte "quanto vale sel?": un case si legge in modo più naturale e si sintetizza più piatto. Le altre tre sono eventi indipendenti con una priorità evidente: qui if/else if è la scelta migliore.
Cosa viene dopo
L'ultima pagina di questo capitolo, For Loops, tratta il for di Verilog e la cosa sorprendente che succede quando lo usi in codice sintetizzabile. Poi entriamo sul serio nella logica sequenziale e nelle FSM.
Domande frequenti
Cos'è l'istruzione case in Verilog?
case (expr) ... endcase è il costrutto di diramazione a più vie di Verilog. Valuta l'espressione una sola volta e passa al ramo che corrisponde. È la scelta idiomatica per macchine a stati, decodificatori di opcode, selettori di mux e qualsiasi altra situazione in cui si sceglie tra diverse opzioni mutuamente esclusive.
Qual è la differenza tra case, casex e casez in Verilog?
case confronta i bit in modo esatto, valori x e z compresi. casez tratta z (e ?) nei pattern come don't care. casex tratta come don't care sia x sia z. Il confronto con don't care è utile per i pattern di opcode in cui alcune posizioni di bit non contano, ma casex è pericoloso in simulazione perché i segnali non inizializzati (x) possono corrispondere per sbaglio a ogni caso.
Perché serve default in un'istruzione case di Verilog?
Senza default, lo strumento di sintesi vede la possibilità che nessun caso corrisponda, conclude che il segnale di uscita deve conservare il valore precedente e deduce un latch indesiderato. Il ramo default gestisce ogni valore non previsto, di solito impostando l'uscita a un valore sicuro oppure segnando il caso come irraggiungibile con un'assegnazione a x. Includilo sempre.
Quando usare case e quando if-else in Verilog?
Usa case quando le condizioni sono mutuamente esclusive e dipendono dal valore di una singola espressione: macchine a stati, decodificatori di opcode, selettori di mux. Usa if/else quando esiste un vero ordine di priorità o quando le condizioni coinvolgono segnali diversi. case si sintetizza in hardware più piatto e veloce rispetto a una lunga catena di else if.