SHIFT REGISTER |
Identificazione progetto
progetto |
autore |
email |
note |
|
Mirco Battistella |
btmpicproject@gmail.com |
Piattaforma: MPLAB X v1.80 Compilatore: Hitec C 9.83 |
Premessa
Il
presente progetto è nato per illustrare le innumerevoli
possibilità offerte
dagli integrati SIPO 74HC595 (Serial In – Parallel Out), in
particolare, per
la loro capacità di espandere il
numero di porte dei microcontrollori e per la compatibilità con i loro livelli di segnale e alimentazione.
Questi dispositivi, in gran parte
sottovalutati
e poco conosciuti, permettono di ricevere serialmente la combinazione
di 8 uscite indipendenti, con la possibilità di mettere in
cascata più
elementi, estendendo così in modo considerevole il numero di
uscite controllabili. C'è da dire che esistono integrati
specializzati per questo scopo, come ad es. il PCF8574, che hanno fra
l'altro la possibilità di impostare indipendentemente i pin sia
come ingressi che come uscite. Normalmente però, questi sono
pilotabili unicamente con un protocollo specifico, come l'I²C, e
hanno un costo decisamente superiore agli shift register.
Verrà
spiegato brevemente il funzionamento e il pilotaggio dei 74HC595 e successivamente
verranno presentate le librerie di interfacciamento e due esempi di utilizzo
in soluzioni dove normalmente si utilizzerebbero un gran numero di pin del
microcontrollore.
Chi fosse interessato alla piattaforma Micro-GT mini potrà richiederla assemblata, oppure kit di montaggio, o solo PCB all'indirizzo ad.noctis@gmail.com
Il 74HC595
Come già anticipato, il 74HC595 fa parte della famiglia degli shift register di tipo SIPO, permette cioè, attraverso l’invio seriale di un byte, di controllare indipendentemente 8 uscite digitali. E' composto da cinque ingressi e nove uscite, più i due pin di alimentazione. Gli ingressi si compongono di un pin seriale DATA, dove viene impostato il dato da trasferire (0 o 1), un pin di CLOCK, che portato dal livello logico 0 a 1, consente di acquisire il valore dell'ingresso DATA e di far scorrere i dati all'interno dello Shift Register, un pin di LATCH che, portato dal livello logico 0 a 1, trasferisce gli otto bit contenuti nello Shift Register allo Storage Register, ovvero il LATCH, un pin di Output Enable, che trasferisce gli otto bit dal LATCH alle uscite, e un pin di RESET, che azzera il contenuto dello Shift Register (non delle uscite!). Da notare che l'Output Enable e il RESET sono negati. Le uscite si compongono di otto pin digitali QA-QH, che assumono i valori caricati nello Shift Register (e di conseguenza nello Storage Register) e di un'uscita digitale QH', che, all'ottavo colpo di CLOCK, assume il valore di QH. Restano i due pin Vcc e GND, ai quali va applicata una tensione tipicamente compresa fra 2V e 6V.
La piedinatura standard è quella riportata nella figura sottostante.
In
pratica, la sequenza per trasferire il byte e attivare le uscite è la seguente:
|
|
|
|
|
|
|
|
|
|
Ecco, a titolo di esempio, quello che si ottiene trasferendo due byte (11010101 - 11001101) ad altrettanti shift register (1 = uscita in ON): |
|
|
Per
collegare in cascata più shift register, si mettono in comune le linee di CLOCK e di LATCH e si collega
l'uscita seriale QH' del primo (pin 9) con l'ingresso seriale DATA del secondo
(pin 14). Al nono colpo di CLOCK, i dati iniziano a passare dal primo shift
register al secondo. Ovviamente, sono necessari Nx8 colpi di CLOCK per trasferire
tutti i dati (dove N è il numero di shift register collegati).
Librerie per l’interfacciamento
Per il controllo software, ho previsto delle librerie in C che consentono di interagire in modo semplice con gli integrati, avendo, in primis, l’accortezza di settare correttamente i 3 pin che si intendono utilizzare per i comandi e il numero di shift register utilizzati. Nulla vieta di modificare le librerie per poter controllare anche le linee Output enable e RESET (non implementate).
Per settare i pin di comando e le costanti utilizzate creiamo, per comodità, degli opportuni #define, in particolare:
#define DATA RB0
#define CLOCK RB1
#define LATCH RB2
#define NUM_SHIFT_REG 3
dove ovviamente al posto di RB0, RB1, RB2, vanno inseriti i pin che intendete utilizzare e al posto del numero 3 va inserito il numero di shift register che state comandando.
Funzioni:
void ImpostaShift (unsigned char valorePorte) { unsigned char shift; unsigned char value; for (shift=0; shift<8;shift++) { value=valorePorte & (0b00000001 << shift); if (value) { DATA=1; } else { DATA=0; } CLOCK=1; CLOCK=0; } LATCH=1; LATCH=0; } |
Partiamo con la prima funzione che serve per trasferire un byte ad uno shift register e successivamente settarne le
uscite.
value = valorePorte & (0b00000001 << shift); es. 1 – value = 0b00101101 & (0b00000001<<3) = 0b00101101 & (0b00001000) =
0b00001000 In questo caso value è diverso da 0, quindi DATA=1 es. 2 – value = 0b00101101 & (0b00000001<<4) = 0b00101101 & (0b00010000) =
0b00000000 In questo caso value è uguale a 0, quindi DATA=0 In base al valore di value, si setta la linea DATA. Si noti che all'interno di if (value), quando value è uguale a 0, DATA è uguale a 0, mentre, in tutti gli altri casi, DATA è uguale a 1: if (value) { DATA=1; } else { DATA=0; } si porta il CLOCK a 1 per trasferire il dato e successivamente lo si riporta a 0 per non creare problemi nelle successive operazioni: CLOCK=1; CLOCK=0; Tutto questo viene ripetuto per 8 volte grazie al ciclo for. Una volta trasferiti tutti gli 8 bit si porta il LATCH a ,1 per settare le uscite e ritrovarci il nostro byte sulle porte del 74HC595. Si riporta infine il LATCH a 0. |
void CaricaShift (unsigned char valorePorte) { unsigned char shift; unsigned char value; for (shift=0; shift<8;shift++) { value=valorePorte & (0b00000001 << shift); if (value) { DATA=1; } else { DATA=0; } CLOCK=1; CLOCK=0; } } |
La seconda funzione, molto simile alla prima, è la base per una funzione più complessa, che serve per settare più integrati in cascata. Infatti, in questo caso, è opportuno attivare il LATCH solo quando sono stati trasferiti i dati di tutti gli elementi connessi. Per far ciò, è stato tolto il comando del LATCH che trasferisce i dati alle porte. Questa funzione, ripetuta N volte tanti quanti sono gli integrati in cascata, permette di caricare un byte per ciascun elemento. Ricordo che per ogni colpo di CLOCK, i dati all’interno dello shift register vengono traslati di una posizione, quindi all'ottavo CLOCK, il bit n. 8 viene presentato all’uscita seriale (pin 9) e, di conseguenza, all’ingresso seriale del successivo integrato, il quale, al nono colpo di CLOCK, acquisirà il segnale ricevuto. |
void ImpostaMultiShift (unsigned char valorePorte[], unsigned char numShiftReg ) { unsigned char numShift; for (numShift=0; numShift<=numShiftReg; numShift++) { CaricaShift(valorePorte[numShift]);
}
LATCH=1;
LATCH=0;
} |
La terza funzione riceve una matrice di char come parametro, in cui ogni elemento della matrice corrisponde al byte da inviare a ciascuno degli shift register in cascata. Ovvio dire che il numero massimo di elementi che accetta sono 255 (char!) e che per avere un’estensione maggiore basta cambiare il tipo di dato della variabile passata. Come prima cosa, viene attivato il ciclo for che trasferisce, elemento per elemento, tutti i dati della matrice, grazie appunto all’utilizzo della precedente funzione CaricaShift: for (numShift=0; numShift<=lunghezzaMatrice; numShift++)
{
CaricaShift(valorePorte[numShift]);}
Quando tutti i byte sono stati caricati, viene portato il LATCH a 1 (e poi di nuovo a 0) e vengono settate le uscite di tutti gli shift register contemporaneamente. lunghezzaMatrice=sizeof(valorePorte/sizeof(int); che permette di stabilire la lunghezza della matrice, e quindi il numero di shift register. In seguito si imposta il limite del ciclo for sulla variabile ottenuta: for (numShift=0; numShift<= lunghezzaMatrice; numShift++) |
void ShiftTest (unsigned char numShiftReg) DATA=1;
for (numShift=0; numShift<numShiftReg;numShift++)
for (shift=0; shift<8;shift++)
{ CLOCK=1; CLOCK=0; } }
LATCH=1;
LATCH=0; DATA=0; }
|
La funzione di test accetta un parametro contenente il numero di elementi in cascata (max 255), porta a 1 la linea DATA
e attiva due cicli for, il primo trasferisce 8 bit a 1 per ogni singolo
integrato, l’altro lo ripete per tutti gli N integrati: for (numShift=0; numShift<numShiftReg;numShift++)
{
for (shift=0; shift<8;shift++)
{ CLOCK=1; CLOCK=0; } }
|
void ShiftReset (unsigned char numShiftReg) { unsigned char shift; unsigned char numShift; DATA=0;
for (numShift=0; numShift<numShiftReg;numShift++){
for (shift=0; shift<8;shift++)
{ CLOCK=1; CLOCK=0; } }
LATCH=1;
}LATCH=0; |
Bene, veniamo ora a due esempi pratici di quanto detto fin’ora.
Controllo di 6 display a 7 segmenti
Nei due esempi che vedremo a breve, verranno utilizzati 2 shift register in cascata
per la gestione in multiplexing di 6 display a 7 segmenti. Il primo shift register viene utilizzato per
l’accensione dei segmenti, mentre il secondo viene
utilizzato per accendere in sequenza i vari display. Nel mio caso ho
utilizzato display a catodo comune con l’utilizzo di 6 transistor
NPN BC337 di pilotaggio. Anche qui, ciascuno può adattare lo
schema in base ai display che ha, avendo cura di modificare i
transistor in PNP qualora si comandi display ad anodo comune. La
parte hardware è comune per entrambi gli esempi ed è
composta da una scheda display e da una scheda di interfaccia per la
Micro GT Mini. La prima è stata "impropriamente" sviluppata su
millefori ( nota personale: sconsiglio vivamente di provarci a meno
che non siate veramente degli autolesionisti!), mentre la seconda è
stata sviluppata con il metodo della fotoincisione su due layer.
Schemi elettrici con descrizioni:
Come si può vedere dallo schema del display, ciascuna uscita del primo shift register è collegata, attraverso una resistenza di limitazione da 330ohm, ai segmenti a, b, c, d, e, f, g, dp di tutti i display, mediante il collegamento parallelo fra questi. Il secondo shift register e collegato ai transistor che comandano i catodi dei display, sempre attraverso la propria resistenza di limitazione da 1k. Come detto, le linee di CLOCK e LATCH dei due integrati sono messe in parallelo, mentre i pin del Output Enable sono collegati a massa e quelli di RESET sono collegati a +5V. Il segnale DATA entra sul primo shift register e, dall'uscita QH', entra nell'ingresso seriale del secondo. Per finire, ho previsto i soliti condensatori di disaccoppiamento nei pin di alimentazione. La routine che si andrà a realizzare, deve tenere conto dei segmenti da accendere in base al display attivo. Tutto ciò, ripetuto in sequenza ciclica e a velocità opportuna, crea l’illusione che tutti i display siano accesi contemporaneamente visualizzando le varie cifre (multiplexing).
Il connettore del display ha 5 pin, di cui:
Riguardo alle resistenze di limitazione dei display a 7 segmenti c'è da fare una precisazione: sul datasheet del 74HC595 è riportato che la massima corrente erogabile dalle singole uscite è di ±35mA, ma che la corrente massima prelevabile contemporaneamente (Icc o IGND) è di soli ±70mA. Questo vuol dire che bisogna calcolare correttamente le resistenze in base al display che si usa ed eventualmente aumentarne il valore (es. 470ohm), a discapito di una minore luminosità. Si consideri comunque che, grazie al multiplexing, è acceso un solo display alla volta e che, normalmente, non vengono mai attivati tutti i segmenti contemporaneamente. Quest'ultima affermazione fa eccezione solo quando viene eseguito il test del display, quindi, se il calcolo delle resistenze che avete fatto è al limite, evitate di eseguirlo.
Nel secondo schema relativo all'interfaccia, ho previsto il collegamento di n. 3 pulsanti sui pin RB4, RB5 e RB6. Questa scelta non è stata fatta a caso: infatti sul PORTB è possibile attivare le resistenze di pull-up (RBPU=0), evitando di montarle fisicamente sul circuito. Inoltre, i pin RB4-RB7 hanno la possibilità di generare interrupt nei cambi di stato, quindi sono decisamente indicati per l'acquisizione di pulsanti.
L'interfaccia ha il inoltre il compito di portare i segnali di pilotaggio e alimentazione alla scheda display, prelevandoli dai rispettivi strip predisposti sulla Micro GT Mini, nonchè di collegare il led n. 1 all'uscita RB3 del pic, per visualizzare il segnale di clock nell'applicazione del esempio n. 2.
Per quanto riguarda le impostazioni della Micro GT Mini, è necessario portare l'alimentazione a +5V sugli strip centrali con un ponticello in morsettiera.
Esempio n. 1: Contatore UP/DOWN
Il primo esempio, molto banale, riguarda un contatore UP/DOWN con un range che va da -60000 a +60000 (i due limiti si possono impostare a piacere). Questo tipo di contatore, con le dovute modifiche, può essere utilizzato in mille applicazioni, dal contapezzi al conta persone, dal contaore al conta impulsi, ecc. All'accensione viene eseguito un ciclo di test del display, attivando per 1 secondo tutti i segmenti. Successivamente, viene azzerato il display e visualizzato il conteggio, che di default è impostato a 0 (posizione centrale nel range). Il pulsante UP incrementa il conteggio, mentre il pulsante DOWN lo decrementa. Quando il numero visualizzato è negativo, i ruoli dei due pulsanti nelle funzioni si invertono: infatti per passare da 0 a -1devo premere il pulsante DOWN, ma in realtà questo internamente incrementa la variabile conteggio da 0 a 1, mentre la variabile sign viene posta a 1 per tenere traccia del segno del conteggio (0=positivo / 1=negativo). Per finire, il pulsante SET mi permette di riportare il contatore a 0. Demando la spiegazione dettagliata ai relativi commenti che ho inserito nel codice.
Esempio n. 2: Orologio digitale
Il secondo esempio riguarda la realizzazione di un orologio digitale. Premetto fin da subito che si tratta di un progetto a scopo didattico basato sul TIMER0, con il solo fine di dimostrare l'utilità degli shift register. Infatti, per la realizzazione di un orologio degno di nota, è consigliato l'uso del TIMER 1 con un clock esterno dato da un quarzetto da 32.768kHz, oppure l'utilizzo di un RTC, che permette di avere, oltre alla buona precisione, anche molte funzioni aggiuntive, quali la data, il giorno della settimana, la sveglia, ecc. In questo caso si vuole dimostrare come, modificando il software, sia possibile utilizzare lo stesso hardware per diverse applicazioni.
Come detto, il clock dell'orologio viene scandito dal TIMER0 che incrementa una variabile clockMs ogni millisecondo. E qui ci sono da dire un'infinità di cose: nella Micro GT Mini in mio possesso è montato un quarzo da 4MHz che non permette di avere l'interrupt richiesto di 1 secondo. Per avere una combinazione sufficientemente precisa, ho previsto di settare il prescaler e il preset del TIMER0 in modo da ottenere l'overflow ogni millisecondo, per poi incrementare la variabile clockMs e aggiornare i secondi dopo 1000 cicli (1000ms=1s). Messa così, la storia funziona, ma è soggetta a grosse imprecisioni. Per questo infatti ho previsto 3 variabili aggiuntive, clockCalib, clockCalibDec e clockCalibMil, che mi permettono di calibrare il conteggio con una risoluzione fino al centesimillesimo di millisecondo. La variabile clockCalib, con range da -100ms a +100ms, mi permette di anticipare o posticipare lo scoccare dei secondi, mentre le due variabili clockCalibDec e clockCalibMil, con range da 0 a 99, aggiungono un ulteriore affinamento della calibrazione lavorando sui centesimi e centesimillesimi di millisecondo. In realtà, le tre variabili potevano essere sostituite da un'unica variabile di tipo float, ma questo complicava di molto sia la parte di correzione dei millisecondi che la parte di salvataggio in memoria. La formula finale che gestisce il clock è:
clock(interrupt)=1000ms±(clockCalib+clockCalibDec+clockCalibMil)
La funzione di interrupt si completa con la gestione dell'incremento dei minuti, delle ore, del lampeggio del led di clock e, in fase di programmazione, del lampeggio dei punti decimali del display.
La programmazione dell'orologio viene effettuata mediante i soliti tre pulsanti SET, UP e DOWN: il pulsante SET mi permette di entrare nella programmazione dell'ora (pressione per più di 1 secondo assieme al pulsante UP), nella programmazione della calibrazione (pressione per più di 1 secondo assieme al pulsante DOWN) e di confermare i parametri visualizzati (pressione singola). Il pulsante UP incrementa il valore dei parametri, mentre il pulsante DOWN lo decrementa. Anche qui, come per il contatore, quando operiamo sulla variabile della calibrazione principale, clockCalib, visto che questa può essere negativa, ho preferito considerare la variabile solo positiva e demandare il segno (positivo o negativo) alla variabile calibSign, con la conseguenza che, in fase di programmazione, le funzioni interne dei due pulsanti si invertono. Da notare che, mentre l'ora e i minuti si azzerano al mancare della tensione di alimentazione o a seguito di reset, le variabili di calibrazione vengono salvate nella memoria eeprom e non si cancellano mai, salvo riprogrammazione del pic.
Per la calibrazione del clock esistono due possibilità: la prima, piuttosto spartana, prevede di controllare "a occhio" la differenza fra il clock del nostro orologio e quello di un orologio campione. In questo caso si verifica nell'immediato di quanto sgarra il clock partendo con le tre calibrazioni a 0. Se il nostro orologio si prende indietro, si setta la calibrazione principale, clockCalib, in negativo, viceversa, se si prende avanti, si setta la variabile in positivo. Trovato il punto in cui il clock dell'orologio si avvicina a quello campione, si riduce la differenza tarando dapprima clockCalibDec e poi clockCalibMil. Ovvio dire che, via via che si affina la calibrazione, è necessario attendere tempi sempre maggiori per poter pretendere di effettuare una correzione precisa che, per la variabile clockCalibMil, possono arrivare anche al mese. La seconda possibilità prevede l'uso di un oscilloscopio (o un frequenzimetro) per verificare il clock sul pin RB3 (o sul led di clock), ponendo anticipatamente le tre variabili di calibrazione a 0. Da qui si vede il reale periodo del nostro segnale e quindi la differenza rispetto al secondo, permettendoci di settare perfettamente le nostre tre variabili. Nel mio caso i valori corrispondenti sono: clockCalib=-8, clockCalibDec=96 e clockCalibMil=5. Come sopra, demando la spiegazione dettagliata ai relativi commenti che ho inserito nel codice.
Esempio di calibrazione "a occhio":
Video
Potete vedere un filmato della Micro-GT mini in funzione al seguente link
Potete richiedere il
vostro esemplare della Micro-GT mini via e-mail all'indirizzo ad.noctis@gmail.com
Questo articolo è solo la base di partenza per capire le potenzialità degli shift register nell'uso combinato con i microcontrollori. Prossimamente, spero di riuscire ad integrare interfaccie più complesse, quali schede a relè modulari e display a matrice di punti, staremo a vedere. Alla prossima!
File correlati
Questo progetto è ridistribuibile
secondo i
termini di licenzaCreative Commons
Attribuzione-Condividi allo stesso modo 3.0 Italia