PI Guide/ Metronomo open source

Come gestire gli interrupt con Arduino per creare uno strumento fondamentale per ogni musicista

In un progetto hardware un microcontrollore (per semplicità ÁC) può essere "solo" una parte (anche se il cuore) di un sistema più ampio che può includere un sistema di immagazzinamento software e/o dati (come una EEPROM, Electrically Erasable Programmable Read-Only Memory), dispositivi di Input/Output (I/O), un front-end analogico e molto altro ancora. Ogni ÁC possiede un proprio set di istruzioni necessarie affinché le circuiterie interne siano a disposizione, via software, di chi voglia utilizzarle in funzione del progetto che ci si accinge a realizzare. Il ÁC a bordo dell'Arduino Uno, l'ATmega328P è un sistema ad architettura RISC caratterizzato da un set di 131 istruzioni. Coloro che volessero approfondire il tipo di istruzioni con associati operandi, possono fare riferimento al paragrafo 36, pagina 432, del Datasheet presente nel file metronomo.tar.gz, un pacchetto che contiene tutto il necessario per seguire al meglio questa guida.

Curiosità: termini da conoscere
Acronimo di Reduced Instruction Set Computer, RISC indica un'architettura nella quale si punta alla minimizzazione del numero di cicli macchina per la maggior parte delle istruzioni che, grazie alla loro semplicità, possono essere eseguite in un solo ciclo di clock spostando così la complessità dell'hardware al software. Di segno opposto l'architettura CISC (Complex Instruction Set Computer) nella quale si è puntato alla potenza delle istruzioni con una complessità tale da necessitare dapprima di una fase di interpretazione seguita dalla loro esecuzione, passaggi che inevitabilmente necessitano di vari cicli di clock. Esempi CISC sono i processori X86/X86_64 a bordo dei nostri PC.

Dall'IDE all'esecuzione
Focalizziamo l'attenzione su un classico programma che possiamo scrivere e in seguito caricare nel ÁC utilizzando l'IDE Arduino. Il programma è in genere caratterizzato da variabili, commenti, funzioni poste tra parentesi graffe (le tipiche sono void setup() e void loop()), istruzioni che terminano con il carattere ; e chiamate a funzioni esterne con i loro argomenti (se accettati). Queste ultime sono le cosiddette API (Application Programming Interface) e fanno parte delle librerie che l'ambiente di sviluppo integrato mette a disposizione (pinMode(), digitalWrite() ecc). Andando a indagare un po' più a fondo - ad esempio, dopo aver installato l'IDE, in /usr/share/arduino/hardware/arduino/avr/cores/arduino - su come sono scritte queste funzioni, troveremo righe del tipo:

TCCR0B = (TCCR0B & 0b11111000) | prescalarbits;
sbi(TCCR0, CS02);
cbi(ADCSRA, ADPS0);

La prima istruzione esegue un'assegnazione del Timer/Counter Control Register B, variabile TCCR0B dopo averne fatto prima un AND bit a bit con la maschera riportata, seguito da un OR bit a bit con la variabile indicata. Il registro è presente fisicamente nel ÁC: ha dimensione di 8 bit e per approfondirne le singole funzioni possiamo leggere il paragrafo 19.9.2, pagina 141, del Datasheet.
La seconda riga è il codice mnemonico di una delle 131 istruzioni supportate dal ÁC: Set Bit I/O (sbi) imposta i bit nei registri di Input/Output. La sua complementare è cbi (Clear Bit I/O) che nello specifico imposta a 0 il bit 0 di nome ADPS0 (ADC Prescaler Select) del registro ADCSRA (ADC Control and Status Register A, paragrafo 28.9.2, pagina 319 del datasheet).
Entrambe, per completare l'operazione, utilizzano 2 cicli di clock.
Per quanto riguarda le istruzioni, invece, queste sono riportate in codice mnemonico (opcode) dell'assembly del ÁC. L'opcode specifica cosa l'istruzione chiede di fare alla macchina. ╚ un nome simbolico che corrisponde a un'istruzione. L'analogia è con gli indirizzi Internet: è più facile ricordare miosito.com e non un indirizzo IP del tipo 72.144.87.198. Nello specifico, i nomi mnemonici come ADD, NEG, COM si ricordano più facilmente rispetto ai corrispondenti codici delle istruzioni che possono essere qualcosa del tipo 1001, 0101 o equivalenti in esadecimale. Gli operando di una opcode specificano i dati sui quali la macchina dovrà operare. Ma nell'IDE Arduino vengono riportate delle istruzioni in un linguaggio di "alto livello", molto simile al C++, il quale non è "comprensibile" dal ÁC. Cosa succede, allora, quando pigiamo sull'icona di caricamento? Nella shell dell'IDE si avvicenderanno una serie di righe. Al di là dei warning, e ipotizzando che non vi siano errori nel codice, principalmente abbiamo varie chiamate al compilatore avr-g++ dove il sorgente viene compilato e assemblato in un file oggetto (estensione .o) ma non linkato. Il linking alle librerie avviene in una chiamata successiva che genererà il programma nel formato eseguibile ELF (Executable and Linkable Format). Solo a questo punto viene invocato il tool avr-objcopy che convertirà il formato ELF in un file HEX (esadecimale) per poi essere caricato nella flash memory del ÁC dal tool avrdude. Allora è intuitivo come, in un programma da decine o centinaia di righe di codice, tutte le istruzioni vengano "trasformate" nei corrispondenti codici equivalenti delle istruzioni supportate dal ÁC. A questo punto, con una descrizione elementare, possiamo dire che il ÁC legge l'opcode della prima istruzione in memoria e la esegue; quindi ripete lo stesso processo sull'opcode disponibile nel byte successivo di memoria programma e così via. Esistono, però, situazioni nelle quali questa modalità potrebbe risultare largamente inefficiente e, in alcuni casi, pericolosa poiché non assicura l'esecuzione di un dato codice entro un certo tempo (deadline) al verificarsi di una data condizione. Per questo motivo la maggior parte dei ÁC possono essere programmati per trattare eventi non schedulati nell'usuale ciclo di esecuzione delle istruzioni. Parliamo di eventi che non si verificano con scadenze temporali fisse e note, risultando asincrone rispetto alla modalità di esecuzione di cui sopra, e che necessitano di un livello di priorità differente a seconda delle esigenze. La reazione del ÁC deve essere opportunamente pianificata dal programmatore al fine di assicurarsi la risposta a eventi di questo tipo. Ma come fa il ÁC a sapere quando si verifica un evento a cui prestare immediatamente attenzione?

Gestione delle interruzioni
L'interrupt, o interruzione, è un evento che permette di modificare l'esecuzione sequenziale delle istruzioni del programma in corso qualora si verifichi una data situazione, accidentale o voluta che sia. Questa condizione deve essere gestita all'atto del suo verificarsi: il ÁC deve sospendere immediatamente l'attuale esecuzione per passare ad una parte specifica (branch) del programma, meglio nota con il nome di Interrupt Service Routine (ISR) con la quale potrà servire l'avvenuta interruzione. Dal punto di vista dell'esecuzione del programma, le cose rimangono perfettamente analoghe a quanto riportato in precedenza: non appena l'ISR termina il suo compito, il ÁC riprende il programma a partire dal punto in cui era stato interrotto. La ISR verrà richiamata nel momento in cui si verificherà - se si verificherà - una nuova interruzione. Il vantaggio della (e nella) gestione delle interruzioni è di fornire una risposta rapida all'evento verificatosi cosa che non sarebbe garantita dall'usuale modalità di esecuzione, ad esempio una lettura ciclica dello stato di una periferica al fine di verificare l'accadimento o meno dell'evento, modalità nota con il nome di polling.

Modalità di interrupt: interne, esterne e mascherate
Giusto un chiarimento. Le possibili situazioni che possono richiedere l'interruzione temporanea del programma in esecuzione sono classificabili in due gruppi: interruzioni esterne, ovvero segnali esterni che condizionano direttamente il pin del ÁC e le interruzioni interne provenienti, ad esempio, da registri, contatori, comparatori e altro presente nel ÁC. Le linee fisiche dedicate agli interrupt in genere sono limitate per questo motivo si ricorre agli interrupt vettorizzati. Ad esempio, la tabella 16.1 a pagina 82 del Datasheet mostra interrupt vettorizzati riportati per ordine di priorità: prima il pulsante di reset seguito dalle due linee di interrupt esterni. Osserviamo come durante l'esecuzione di un programma sia possibile disabilitare le interruzioni "mascherandole" (istruzione assembly cli) per poi riabilitarle (istruzione sei). Queste istruzioni scrivono uno 0 o un 1 in uno specifico registro.

Dal punto di vista pratico l'interruzione si identifica in una variazione del livello di tensione (condizione logica) su un dato pin del ÁC. Questa situazione è gestita direttamente all'interno del ÁC e, tramite apposite istruzioni, è controllabile via software. Per il ÁC utilizzato nella Arduino Uno due sono le funzioni riservate allo scopo: interrupts() e attachInterrupt() con le corrispondenti funzioni per la disabilitazione dell'interrupt, noInterrupts() e detachInterrupt(). Dal punto di vista fisico l'ATMega328 presenta due linee esterne di interrupt associate ai pin 4 e 5 (ingressi digitali 2 e 3 della scheda Arduino Uno). A intervalli regolari il ÁC verifica lo stato dei flag di interrupt e qualora corrisponda alla condizione impostata via software esegue la ISR la quale può essere eseguita in diverse modalità. La frequenza con la quale il ÁC esegue la ISR è visibile nella figura seguente.


Linea di interrupt e esecuzione della ISR

Come è stato ottenuto questo risultato? Dopo aver impostato la modalità low, al pin 4 del ÁC (pin 2 uscite digitali sul connettore della Arduino Uno) è stato collegato un generatore di segnali alla frequenza di 1kHz (traccia blu). L'uscita - traccia gialla - è stata presa al pin 13 del ÁC (pin 8 uscita digitale della scheda). Questo semplice test mostra come l'esecuzione della ISR avvenga ad una frequenza di poco inferiore ai 50kHz (un periodo di circa 20Ás). Sempre nella figura precedente, la parte in basso riporta la frequenza di esecuzione della ISR per la ISR gestita in modalità rising (fare riferimento allo sketch Test_interrupt.ino).

Trasduttori di posizione angolare
Noti anche con il nome rotary encoder (encoder rotativi), nell'accezione più generica è un dispositivo che permette di codificare in una sequenza di segnali elettrici la rotazione di un albero. In commercio si possono avere diversi tipi di encoder in funzione del principio di funzionamento sul quale si basano: meccanici, ottici (laser, infrarossi) o sfruttando l'effetto Hall. Il principio di funzionamento può essere ricondotto a degli interruttori che ciclicamente, in funzione della rotazione dell'albero, chiudono dei contatti fornendo così su di essi una condizione logica 0 (tensione 0 V) oppure una condizione logica 1 (massima tensione positiva). Si possono differenziare in encoder assoluti e relativi. Gli encoder assoluti forniscono un segnale (binario o in codice Gray) che indica in ogni istante la posizione dell'albero: in caso di spegnimento del sistema, alla successiva riaccensione, fornisce l'esatta posizione dell'albero. Gli encoder assoluti possono essere a singolo giro fino a 4096 giri.

A seconda del modello il segnale fornito può essere trasferito con una interfaccia parallela, su bus seriale (SSI - Synchronous Serial Interface) oppure possiedono già un'interfaccia per bus Profibus, CanBus o InterBus. Gli encoder incrementali forniscono due forme d'onda squadrate e sfasate tra di loro di 90 gradi (1/4 di periodo) e solitamente indicate con le lettere A e B: per tale motivo vengono definiti anche encoder incrementali in quadratura. Dalla lettura di un solo canale si può risalire alla velocità di rotazione e l'acquisizione del secondo canale permette di ricavarne anche il verso. Esistono encoder incrementali che presentano un terzo canale detto Z o Zero che fornisce un impulso sul punto zero dell'albero. Altri tipi di encoder incrementali, più complessi, ad esempio utilizzati come segnali di retroazione su motori elettrici come gli encoder incrementali con segnali di commutazione integrati che esulano da questo contesto.

Eliminare conteggi impropri
Gli encoder più economici - che per tali progetti si rivelano anche i più pratici da utilizzare - vedono al loro interno dei contatti striscianti i quali, in funzione della posizione dell'asse, attivano questa o quella uscita. Contatti meccanici che generano qualche problematica: oltre all'usura con il tempo (il produttore dichiara nei propri datasheet un ciclo di vita medio) abbiamo anche il problema legato ad una commutazione meccanica che genera il cosiddetto "rimbalzo" (bounce). Cos'è un rimbalzo? Pensiamo ad un pulsante normalmente aperto (in gergo "N.A."). Quando premuto per chiudere il circuito non passa mai istantaneamente alla posizione di chiuso ma, a causa delle asperità superficiale dei contati nonché in funzione della pressione esercitata e dell'assestamento della molla nel caso di interruttori, nei primi millisecondi si hanno delle alternanze di chiuso-aperto che provocano i cosiddetti "rimbalzi". Se un comportamento di questo tipo non crea alcun problema qualora si debba accendere una lampadina o un impianto di auto-ritenuta per motori elettrici, problemi invece sorgono nel momento in cui questo pulsante è collegato ad un circuito elettronico, ad esempio un contatore poiché anche se pigiamo il pulsante una sola volta, a causa del rimbalzo vengono conteggiati più impulsi. Esistono diversi modi per realizzare un sistema di debouncing ("anti-rimbalzo"). Il più semplice e immediato è un debouncing hardware utilizzando un filtro passa-basso RC. Nella figura seguente è visibile il primo prototipo: il gruppo di resistenze/condensatori presenti vicino all'encoder realizzano il filtro in questione.


Una panoramica del metronomo su breadboard

Per lo schema elettrico e il cablaggio su breadboard fare riferimento al file Metronomo.fzz utilizzando il programma Fritzing. Se abbiamo a disposizione un oscilloscopio, l'utilità del filtro anti-rimbalzo diventa ancora più evidente se effettuiamo un semplice esperimento. Dallo schema elettrico del circuito completo estrapoliamo e realizziamo in pratica solo la parte associata all'encoder con i due filtri RC.


Schema test per la valutazione dei rimbalzi dovuti ai contatti dell'encoder

Colleghiamo due canali dell'oscilloscopio prima del filtro RC e alimentiamo il circuito. Ruotiamo l'albero dell'encoder. Avremo due onde in quadratura apparentemente quadre (schermo in alto nella metà superiore della figura seguente) la cui durata è funzione della velocità di rotazione dell'albero.


Eliminare gli impulsi spuri con un filtro RC

Ingrandendo una parte che racchiude il fronte di salita vedremo una serie di impulsi spuri (schermo in alto nella metà inferiore della figura). Eseguiamo la stesa prova collegando ora i due canali dell'oscilloscopio dopo il filtro RC. Il risultato, come evidente nella foto in basso, mostra assenza totale di impulsi spuri con fronte di salita addolcito.
Notizie collegate