Progetto completo di un World Clock realizzato con il PIC18F4620, completamente open source. Inizialmente verrà mostrato l'hardware di un orologio che usa le parole al posto dei numeri. Successivamente si descriverà il software con le routine che possono essere utili più in generale. Tutto il progetto può anche essere visto come un esercizio, con diversi spunti da prendere, e ce ne sono molti, dalla gestione della comunicazione I2C alla definizione di una macchina a stati finiti per gestire procedure Time-Safe attraverso interrupt.
Ed infine si proseguirà con la descrizione dell'assemblaggio, del tutto in una cornice, nel vero senso della parola, adatta.
Introduzione
Molti maker hanno in mente un progetto che ritengono abbastanza importante per iniziare a lavorarci. Un progetto interessante, divertente ma non abbastanza "professionale". Con questa idea in mente però continuano ad accumulare informazioni e componenti in un cassetto per usarli “un giorno”.
In un periodo di lavoro (quello vero non l'hobby) molto intenso, ho sentito il bisogno di liberare la mente dalla routine giornaliera divagando su qualcosa di semplice ma allo stesso tempo stimolante, utile ma divertente. Era il momento giusto per aprire il cassetto e tirare fuori il progetto messo da parte per troppo tempo.
Provate a cercare con Google "word clock". Troverete un enorme quantità di siti come questo [1]. Molti basati su Arduino, perfettamente descritti e pronti per funzionare ... per chi si accontenta. Non stavo cercando una soluzione rapida e semplice. Il mio obiettivo è il progetto non il risultato.
Quindi, dopo tutto, anche un progetto apparentemente semplice da realizzare, in realtà contiene molti spunti con soluzioni hardware o routine software da ri-utilizzare anche in altre occasioni.
La fase più divertente del progetto: la progettazione dell'hardware, recuperando tutti i componenti raccolti negli anni.
La realizzazione del pannello che conterrà la matrice di LED per visualizzare le parole.
Da un vecchio acquisto cumulativo presso un negozio on-line ho messo da parte 500 LED bianchi ad alta luminosità.
Da un acquisto ancora più vecchio ho alcuni fogli di PVC espanso 20x20cm.
Utilizzando Adobe Illustrator ho realizzato le maschere trasparenti con le lettere e ho iniziato quindi a sperimentare la stampa con diversi tipi di stampa, sia a getto d'inchiostro che laser.
Un'altra fase divertente del progetto è la scrittura del software. Usando le giuste tecniche tutti i pezzi lavoreranno all'unisono, come vedremo in seguito.
Alla fine occorre assemblare tutti i pezzi insieme e finalmente vedremo come far diventare questo orologio un pezzo di arredamento che non sfiguri anche in un ambiente diverso da un laboratorio.
Hardware
Schemi e spiegazioni
Matrice LED
come multiplexare righe e colonne
Molti dei circuiti che si trovano in Rete usano alcune serie di LED collegati insieme per comporre le parole con poche linee di I/O del microcontrollore. Per questo progetto volevo invece avere un circuito di uso generale, utilizzabile per diverse lingue. Questo richiede ovviamente una matrice di LED pilotata da I/O con una scansione X-Y ad una velocità sufficiente. Per usare lingue diverse basta semplicemente modificare il software e la maschera delle lettere.
LED istallati e connessi sul panello (retro).
Questa è una classica configurazione a matrice [2]. Multiplexando i segnali in modo corretto è possibile illuminare ogni LED singolarmente.
I LED ad alta luminosità non possono essere pilotati in una configurazione a matrice 10 x 12 direttamente tramite le porte I/O del microcontrollore. C'è bisogno di alcuni driver, sia di tipo high-side che low-side. I driver high-side sono utilizzati per le colonne, quelli low-side per le righe. Le colonne (12) sono pilotate in parallelo ad ogni scansione delle righe (10). Questo permette di avere un refresh completo del quadro ogni 1/10 del tempo totale. Applicando la scansione sulle colonne si avrebbe invece un tempo per ognuna di 1/12 del tempo totale. Questo consente ad ogni LED di rimanere acceso per il 10% del ciclo di lavoro, invece del 8,3%, con un certo aumento della luminosità risultante a causa dell'operazione di multiplex, il tutto associato ad una gestione più semplice.
Schema elettrico equivalente del circuito di pilotaggio dei LED.
Come avviene l'indirizzamento di ogni singola lettera. Ad esempio: per illuminare la parola "SONO" è necessario attivare le colonne ABCD nel momento esatto in cui la fila 0 è abilitata.
Dopo un po' di prove pratiche sono emerse alcune correzioni da apportare al software di controllo che permettono un notevole risparmio sulla complessità dell'hardware.
Utilizzando un ulteriore timer disponibile nel PIC18F è possibile attuare la regolazione della luminosità PWM senza complicare il circuito. All'inizio avevo pensato di ottenere una regolazione della luminosità tramite il classico PWM ma, in questo modo avrei avuto bisogno di ulteriori circuiti di potenza per alimentare le colonne in comune con una corrente uguale alla somma delle singole correnti.
Ho pensato invece di ottenere una “parzializzazione” del tempo di accensione usando un secondo timer con una notevole semplificazione hardware a scapito di una complessità del software solo leggermente maggiore. Il timer TMR3 è utilizzato per pilotare il multiplexing della matrice, commutando le righe in successione e illuminando le colonne corrispondenti alla parola da visualizzare. Nella Routine relativa a questo timer (che lavora ad interrupt) è avviato il timer TMR1 in modalità one-shot con un periodo pre-calcolato secondo il duty-cycle desiderato. Quando TMR1 va in overflow spegne tutte le colonne. In poche parole: con TMR3 si accende con TMR1 si spegne .
Ad esempio: TMR3 periodo = 1 ms (in modo da avere un frame-rate con periodo da 10ms=100Hz), se TMR1 = 500us il ciclo di lavoro è del 50%, se il periodo è 1ms il duty cycle è al 100%, sempre con una velocità di scansione di 100Hz.
In ogni caso, è meglio fermare la luminosità massima al 99% per evitare sovrapposizioni con la successiva fila nella scansione provocando un "effetto ombra" a causa del ritardo di spegnimento dei driver.
La sincronizzazione dei segnali è molto importante, anche con una matrice di 10 x 12. Su una velocità di scansione a 100Hz anche gestire i microsecondi può portare a criticità. Ci si deve prendere cura anche dei tempi di ritardo dei driver. L'UDN2981 (usato per pilotare le colonne sul lato alto) ha un ritardo di spegnimento più grande di 3 microsecondi. Il LN2003 (sulle righe, dal lato basso) è dieci volte inferiore. Con un MCU a 10Mips è sufficiente spostare il codice che porta a ON o a OFF anche solo pochi cicli dopo per provocare una sovrapposizione dei segnali. Pochi microsecondi di accensione accendono, anche se solo debolmente, i LED ad alta luminosità. Bisogna stare molto attenti alla “dead band” tra le diverse commutazioni.
L'utilizzo di un analizzatore logico ha facilitato molto la comprensione di tutto il comportamento del circuito.
Questi sono i segnali che scandiscono le 10 righe. Mantenendo ognuno alto per 1ms si ottiene un quadro completo ogni 1ms x 10 = 10ms = 100Hz.
I segnali relativi alle colonne
A0 e A1 sono i primi due segnali di riga. Gli altri sono legati alle colonne da A a L. Sono ON per 0.5ms per ottenere un duty cycle del 50%. In questo esempio, tutta la riga 0 è illuminata a metà.
In quest'immagine una riga ed una colonna con una zona morta di 100ns. Dopo alcuni altri test, ho deciso di aumentarla.
Quanta corrente serve per accendere i LED?
I LED ad alta luminosità bianchi richiedono 20mA per funzionare correttamente. Con un multiplexing su 10 righe, questi sono ON per 1/10 di tutto il periodo di frame. Grazie alla persistenza della retina, una scansione a 100Hz viene vista come una luce continua, ma la luminosità equivalente è minore . In teoria dovrebbero essere pilotati con una corrente di 10 volte superiore per avere la luminosità corretta. Ma, nella pratica, niente è perfettamente lineare.
Analizzando le caratteristiche di un generico LED bianco ad alta luminosità, possiamo vedere che, aumentando di 2,5 volte la corrente (50mA) abbiamo solamente un raddoppio dell'intensità luminosa. Con 4x solo 2,7 volte la luminosità. Come si poteva supporre, il comportamento non lineare rende impossibile raggiungere le (teoricamente) necessarie 10 volte.
Una buona scelta per evitare una morte prematura dei LED, potrebbe essere una corrente di 50mA.
Intensità luminosa relativa in funzione della corrente diretta di un LED generico bianco.
Leggendo la scheda tecnica [3] si può notare una corrente diretta massima If= 100 mA con un 10% di duty cycle @ 1kHz. Calcolando il miglior compromesso con una buona luminosità, una corrente non molto elevata per i LED e la capacità massima dei driver, si arriva ad un più ragionevole valore pratico di 40mA. Considerando anche la caduta di tensione di entrambi i driver, tale corrente si ottiene inserendo un resistore da 2.2Ohm con un'alimentazione di 6.5V.
I calcoli precedenti sono stati fatti per ottenere il miglior compromesso tra luminosità massima e gestione dell'hardware. Nella pratica, per evitare una luminosità fastidiosa in una stanza buia e allo stesso tempo una buona visibilità in un luogo assolato, la luminosità (cioè: il duty cycle del PWM) viene regolata automaticamente leggendo la luce ambiente tramite una fotoresistenza.
MCU
PIC18F4620
Per gestire gli I/O e le diverse periferiche con la giusta velocità e, soprattutto, con l'esatta temporizzazione, è necessario avere il pieno controllo sull'hardware. Il solito Arduino è molto comodo da usare tramite il suo IDE che nasconde tutte le noiose configurazioni delle periferiche ma, proprio per questo, non è adatto a pilotare 10 file di LED con una scansione a 100Hz (quindi una temporizzazione di 1ms) mentre scambia i dati sul bus I2C con il Real Time Clock e in seriale con il modulo WiFly.
E allora?
Un PIC18F4620 a 10 MIPS è più che adatto allo scopo.
Usando nel modo corretto e con le giuste priorità le periferiche e i relativi interrupt, è possibile ottenere delle temporizzazioni molto precise e costanti.
Lo schema elettrico della parte relativa al PIC18F4620.
Il livello del pin TX della seriale verso il WiFly è adattato da 5.5V a 3.3V tramite un partitore.
Il ben noto e sperimentato PIC18F4620 a 10 MIPS [4] è il giusto compromesso tra potenza e complessità.
Quasi tutti i pin di questa versione a 40 pin sono utilizzati per pilotare le righe e le colonne, il bus I2C, RX e TX seriale, il quarzo oscillatore, LED di heart-beat e l'ICSP.
*------Port A
* RA0=LDR An.
* RA1 = Row 0
* RA2 = Row 1
* RA3 = Row 2
* RA4 = Row 3
* RA5 = Row 4
*------Port B
* RB0 = INT 0
* RB1 = Row 7
* RB2 = Row 8
* RB3 = Row 9
* RB4 = _____
* RB5 = LED y
*------Port C
* RC0 = Row 5
* RC1 = Row 6
* RC2 =______
* RC3 = I2C C
* RC4 = I2C D
* RC5 = Col L
* RC6 = -TX--
* RC7 = -RX--
*------Port D
* RD0 = Col A
* RD1 = Col B
* RD2 = Col C
* RD3 = Col D
* RD4 = Col E
* RD5 = Col F
* RD6 = Col G
* RD7 = Col H
*------Port E
* RE0 = Col I
* RE1 = Col J
* RE2 = Col K
Lo Sparkfun I2C Real Time Clock [5] è usato per mantenere il tempo di sistema. Questo ha un'uscita a 1Hz (SQW) usato per avviare la lettura di ore, minuti e i secondi via bus I2C ogni secondo da parte dell'MCU.
Per correggere la deriva a lungo termine del RTC, il tempo è sincronizzato tramite Internet per mezzo del modulo Sparkfun WiFLy [6], con il riferimento di tempo campione dell'INRIM [7] di Torino, quello che una volta si chiamava Istituto Elettrotecnico Nazionale (IEN).
La tensione prelevata da un classico alimentatore a parete (da 9 a 36V) viene abbassata e stabilizzata a 6.5V, con un altissimo rendimento, attraverso uno switching regulator [8]. Due regolatori lineari sono utilizzati poi per fornire al modulo WiFly la 3.3V e al PIC18F4620 la 5V.
Tutti i componenti trovano posto in modo molto compatto su una “millefori”, collegati tra loro tramite filo per wire-wrap.
Un test completo della matrice per la visualizzazione di alcuni modelli dinamici e anche alcuni caratteri aggiungendo al codice un generatore di caratteri 5x7.
In questo video [9] il duty cycle è fissato al 50%.
LINKS capitolo Hardware:
[1] http://www.elektronika.ba/841/word-clock/
[2] http://www.appelsiini.net/2011/how-does-led-matrix-work
[3] http://www.guiott.com/WC/OVLEW1CB9-TT-datasheet-5394461.pdf (White High-Intensity LED Lamp (5 mm, 15°Viewing Angle)
[4] http://www.microchip.com/wwwproducts/Devices.aspx?dDocName=en010304
[5] https://www.sparkfun.com/products/12708
[6] https://www.sparkfun.com/products/retired/9333
[7] http://www.inrim.it/n/index.php
[8] http://it.rs-online.com/web/p/regolatori-switching/6664372/
[9] https://www.youtube.com/watch?v=olsO2BnUBeQ&feature=youtu.be
Software
Come far parlare tra loro periferiche lente e veloci
Dopo aver visto la realizzazione dal punto di vista Hardware, passiamo alla descrizione del Software.
Il primo problema da affrontare è quello di visualizzare le parole relative alle ore in modo stabile e senza sfarfallio. Per avere un aggiornamento ogni 1 ms, bisogna porre un'attenzione particolare perché i diversi task non interferiscano tra loro. Questo richiede un programma completamente “interrupt driven” assegnando la giusta priorità a ciascuna attività.
Vediamo intanto come comunicare con il modulo Real Time Clock in I2C per aggiornare l'orologio con i giusti valori.
Le procedure I2C sono state sviluppate per essere integrate in un programma completamente interrupt driven, non usano quindi polling o cicli di attesa. L'header standard i2c.h è usato solo per alcune definizioni e per l'inizializzazione. Analizzando il codice sorgente della libreria I2C si può notare come in alcuni casi ci siano dei loop di attesa per l'esecuzione di alcune operazioni della periferica SSP che, ovviamente, non sono compatibili con una struttura in tempo reale.
Un altro compito da eseguire parallelamente alle procedure “time critical”, è la comunicazione seriale con il modulo WiFly, che scambia i dati a 9600 bps. Per evitare blocchi del programma a causa di un bus così lento, tutte le procedure di basso livello devono essere pilotate in interrupt dal modulo USART, utilizzando una macchina a stati finiti (FSM) che gira in modo asincrono con il flusso del programma.
La FSM è sviluppata in modo generico in modo da essere utilizzabile per qualsiasi scambio dati senza dover riscrivere il codice, ma solo cambiando una semplice struttura.
Dopo un'attenta analisi delle problematiche elettriche connesse alla scansione della matrice LED, analizziamo il software che pilota direttamente le porte di I / O.
Procedure I2C
Tutte le procedure critiche sono state riscritte in modalità time-safe utilizzando gli interrupt SSP (Synchronous Serial Peripheral), gli indicatori di stato e le variabili globali in una macchina a stati finiti.
Una traccia utile per usare questo tipo di tecnica è riportata nel documento Microchip AN736 [1].
La comunicazione I2C avviene attraverso diversi livelli.
Nel ciclo principale i flag sono controllati iniziando dal basso. L'intero ciclo viene ripetuto per ciascun dispositivo, se necessario, con la scansione dei buffer riempiti da procedure specifiche per ognuno dei dispositivo.
Macchina a Stati Finiti [2]
-
L'evento I2C è comunicato dalla periferica SSP attraverso interrupt. La procedura I2cLowService esegue la sequenza I2C per scambiare un singolo byte, azzera l'evento I2C o il flag di collisione del bus e, una volta scambiato tutto il buffer per quel dispositivo specifico, ripristina I2cBusyFlag per abilitare la routine di livello superiore.
-
I2cBusyFlag è impostato da I2cHighService e resettato da I2cLowService.
-
La routine specifica per il dispositivo ha riempito il buffer, impostato i flag I2cHighService e inizializzato la sequenza I2cLowService. Questo per lo scambio di ogni singolo byte. Ad ogni byte scambiato il contatore viene diminuito. Quando il contatore TX è = 0 inizia la ricezione. Quando entrambi i contatori sono = 0 si può iniziare con il dispositivo successivo, se necessario.
-
Tutti i buffer dei dispositivi sono stati utilizzati, al ciclo successivo riparte dal primo dispositivo.
-
E' pronto per controllare il dispositivo successivo al ciclo seguente.
Livello applicativo [3]
Ogni dispositivo I2C ha un proprio buffer (4 byte per RX e 4 byte per TX). La routine specifica del dispositivo riempie il buffer con i byte per lo scambio e imposta i flag secondo le azioni da eseguire:
I2C [I2cDevPtr] .Flag.Rx
I2C [I2cDevPtr] .Flag.Tx
309 void I2cData (unsigned char DevPtr, unsigned char TxBytes,
310 unsigned char TX1, unsigned char TX2, unsigned char TX3,
311 unsigned char TX4, unsigned char RxBytes)
312 {// Exchange data with I2C device
313 I2c[DevPtr].Flag.Tx = TxBytes; // n bytes to TX
314 I2c[DevPtr].TxBuff[0] = TX1 ; // first byte
315 I2c[DevPtr].TxBuff[1] = TX2 ; // second byte
316 I2c[DevPtr].TxBuff[2] = TX3 ; // third byte
317 I2c[DevPtr].TxBuff[3] = TX4 ; // fourth byte
318 I2c[DevPtr].Flag.Rx = RxBytes; // n bytes to RX
319 I2c[DevPtr].Done = 0; // reset flag, wait for execution
320 }
Livello superiore (I2cHigh) [4]
La procedura ad alto livello viene eseguita se il dispositivo indirizzato ha qualcosa da scambiare:
I2C [I2cDevPtr] .Flag.Rx! = 0 || I2C [I2cDevPtr] .Flag.Tx! = 0
e se la routine livello inferiore è disponibile:
I2cBusyFlag = 0.
Scambia i byte attraverso la routine di livello più basso e imposta il I2cBusyFlag.
239 void I2cHighService (void)
240 {
241 /*
242 initialize I2C sequence resetting flags, counters and buffers
243 kick start
244 all the following procedures will be triggered by interrupts
245 */
246 Ptr.Tx = 0; // reset TX buffer pointer
247 Ptr.Rx = 0; // reset RX buffer pointer
248 I2c[I2cDevPtr].RxBuff[0] = 0; // reset RX buffer
249 I2c[I2cDevPtr].RxBuff[1] = 0; // reset RX buffer
250 I2cEventFlag = 0;
251 I2cBusCollFlag = 0;
252 I2cBusyFlag = 1;
253 I2cStat = START; // FSM pointer
254
255 StartI2C(); // start with first status
256
257 }
Livello inferiore (I2cLow) [5]
La procedura a basso livello è la FSM propriamente detta. Questa è eseguita se la Interrupt Service Routine ha messo a 1 l'I2cEventFlag.
-START->ADRW-> se c'è qualcosa da trasmettere TX
altrimenti Rx
-WRITE byte[0]->...WRITE byte[n]->
-RSTART->ADRR->
-READ byte[0]->ACK...READ byte[n]NACK->
-STOP
-FINE
118 void I2cLowService (void)
119 {
120 I2cEventFlag=0; // wait for next interrupt
121
122 if (I2cBusCollFlag || SSPCON2bits.ACKSTAT)
123 { // Collision on bus or NACK -> sequence aborted
124 I2cBusCollFlag = 0; // error status reset
125 I2cStat = FINE; // current status = FINE
126 StopI2C(); // sends stop
127 // it doesn't reset RX & TX counter to retry on next round
128 }
129
130 else
131 {
132 // execute scheduled sequence for each byte in the selected record
133
134 switch (I2cStat)
135 {
136 case (STRT): // START sequence
137 if (I2c[I2cDevPtr].Flag.Tx > 0)// something to send?
138 {
139 I2cStat = WRITE; // next status
140 SSPBUF = I2cAdr[I2cDevPtr]; // update flag: R/W = write
141 }
142 else // if no bytes to send it has some byte to receive
143 {
144 I2cStat = READ; // next status
145 SSPBUF = I2cAdr[I2cDevPtr]+1;// update flag: R/W = read
146 }
147 break;
148
149
150 case (WRITE):
151 /* sends Nth byte
152 all bytes sent?
153 all bytes received?
154 otherwise stops sequences
155 */
156 SSPBUF = I2c[I2cDevPtr].TxBuff[Ptr.Tx]; // TX first byte
157 Ptr.Tx ++; // points to next byte
158 if (Ptr.Tx >= I2c[I2cDevPtr].Flag.Tx) // all bytes sent?
159 {
160 if (I2c[I2cDevPtr].Flag.Rx > 0) // all bytes received?
161 {
162 I2cStat = RSTART; // starts RX sequence
163 }
164 else // nothing to receive
165 {
166 I2cStat = STP;
167 }
168 }
169 else // still something to TX
170 {
171 I2cStat = WRITE; // TX next byte(s)
172 }
173 break;
174
175
176 case (READ):
177 SSPCON2bits.RCEN = 1; // starts RX sequence
178 Ptr.Rx ++; // next byte Rx
179 if (Ptr.Rx >= I2c[I2cDevPtr].Flag.Rx) // all bytes received?
180 {
181 I2cStat = NACK; // YES, send NACK (Rx over)
182 }
183 else
184 {
185 I2cStat = ACK; // NO, send ACK (Rx proceed)
186 }
187 break;
188
189
190 case (ACK):
191 I2c[I2cDevPtr].RxBuff[Ptr.Rx-1] = SSPBUF; // store Nth byte received
192 I2cStat = READ; // more bytes
193 AckI2C();
194 break;
195
196
197 case (NACK):
198 I2c[I2cDevPtr].RxBuff[Ptr.Rx-1] = SSPBUF; // store Nth byte received
199 I2cStat = STP; // last byte received
200 NotAckI2C();
201 break;
202
203
204 case (RSTART):
205 // reinit bus without release
206 I2cStat = ADRR; // next status = start RX
207 RestartI2C();
208 break;
209
210
211 case (ADRR):
212 I2cStat = READ; // next status = RX
213 SSPBUF = I2cAdr[I2cDevPtr]+1;// update flag: R/W = read
214 break;
215
216
217 case (STP):
218 I2cStat = FINE; // next status
219 I2c[I2cDevPtr].Flag.Rx = 0; // no more bytes to RX or TX
220 I2c[I2cDevPtr].Flag.Tx = 0; // high level procedures can start again
221 StopI2C(); // send stop
222 break;
223
224
225 case (FINE):
226 I2cBusyFlag = 0; // I2C comm over
227 I2c[I2cDevPtr].Done = 1; // procedure complete for this device
228 break;
229
230
231 default:
232 break;
233
234 } // end switch
235 } // end else
236
237 }
Interrupt Service Routine [6]
L'intera macchina a stati finiti è pilotata da interrupt della periferica SSP
255 // High priority interrupt vector
256 void interrupt high_isr (void)
257 {
258 .......
293
294 if (PIR1bits.SSPIF) // an I2C event is over
295 {
296 PIR1bits.SSPIF = 0; // reset I2c interrupt flag
297 I2cEventFlag = 1; // I2cLowService will be executed
298 }
299
300 if (PIR2bits.BCLIF) // I2c bus collision
301 {
302 PIR2bits.BCLIF = 0; // interrupt flag reset
303 I2cEventFlag = 1; // execute I2cLowService
304 I2cBusCollFlag = 1; // a collision occurred
305 }
306 }
L'intero progetto MPLABX C per il PIC18F4620 è disponibile come open source in GitHub repository [7]
Una procedura di comunicazione seriale multiuso
Per essere pilotato il WiFly ha bisogno di una serie di comandi molto semplici, come ben spiegato nella scheda tecnica [8] e di seguito in alcuni esempi di questa pagina [9]. Il protocollo è semplice ma lento, non strutturato e differisce caso per caso, sia dal punto di vista dei tempi sia della sintassi. Ancora una volta una macchina a stati finiti asincrona schedulata da interrupt è perfetta per essere integrata con altre procedure time-critical. Ma non volevo scrivere un programma "hard-coded" per ogni diversa sequenza di comandi. Il mio obiettivo era quello di realizzare un generica procedura polivalente applicabile per diverse operazioni Wifly e riutilizzabile anche in situazioni diverse.
Per essere compatibile con altre procedure critiche, la sequenza di comunicazione deve essere sviluppata su più strati. Il livello superiore inizia con il TX o l'RX di una stringa e tutto deve funzionare autonomamente pilotato solo tramite interrupt della periferica USART. La schedulazione della sequenza della procedura deve essere gestita da una FSM facilmente configurabile. Questo è possibile tramite un uso intensivo dei puntatori.
Definizione della struttura [10]
Per ogni sequenza dobbiamo definire una struttura con alcuni campi definiti.
Il primo campo è il codice di operazione da eseguire.
Il secondo è la stringa da trasmettere o che deve essere ricevuta come terminatore durante la ricezione.
Il terzo è un puntatore a una funzione da richiamare alla fine di ogni passo della FSM.
33 typedef char (*FsmCallbackFunc)(void); // pointer to function
34 .....
50 struct FsmTable * FsmStructActive;
51
52 struct FsmTable
53 {// Scheduler for Finite State Machine
54 unsigned char Stat; // status index
55 const unsigned char * Str; // string to send or receive
56 FsmCallbackFunc pCallback; // function to call after RX or TX is over
57 };
58
59 enum Fsm{FsmRx, FsmTx, FsmDo, FsmEnd};
Programmazione della FSM [11]
In questo esempio, quando la stringa relativa all'orario è completamente ricevuta, deve essere decodificata. Nei passaggi precedenti chiama semplicemente una callback fittizia che non fa nulla.
68 struct FsmTable ReadTimeFsm[] =
69 {/* scheduler used for comm protocol with WiFLy
70 this reads time from http://www.inrim.it
71 */
72 {FsmTx, "$$$", (FsmCallbackFunc)CommFsmIdle},
73 {FsmRx, "CMD", (FsmCallbackFunc)CommFsmIdle},
74 {FsmTx, "open\r\n", (FsmCallbackFunc)CommFsmIdle},
75 {FsmRx, "*CLOS*", (FsmCallbackFunc)TimeDecode},
76 {FsmDo, "", (FsmCallbackFunc)CommFsmWriteRTC},
77 {FsmEnd, "", (FsmCallbackFunc)CommFsmClear}
78 };
Un altro esempio di una sequenza che invia solo un carriage return/line feed e attende la stringa "EXIT" senza chiamare alcuna altra funzione.
61 struct FsmTable ExitCmdFsm[] =
62 {// scheduler used to exit from command mode
63 {FsmTx, "exit\r\n", (FsmCallbackFunc)CommFsmIdle},
64 {FsmRx, "EXIT", (FsmCallbackFunc)CommFsmIdle},
65 {FsmEnd, "", (FsmCallbackFunc)CommFsmIdle}
66 };
FSM start [12]
Dopo aver definito le operazioni da eseguire nella FSM, dobbiamo chiamare solo l'avvio della FSM puntando alla struttura desiderata.
Naturalmente dobbiamo anche costruire le procedure necessarie per richiamarle.
Dopo di che la macchina viene autonomamente pilotata solo tramite l'interrupt USART o TMR1 timeout in caso di errore di comunicazione.
Un flag dice alla procedura di livello applicativo se la FSM è libera di eseguire un'altra sequenza.
72 void StartCommFsmSched(struct FsmTable * FsmStruct)
73 {// initialize the FSM
74 FsmIndx = 0;
75 CommFsmFlag = 1; // kick off the FSM
76 TxFsmFlag = 0;
77 RxFsmFlag = 0;
78 FsmStructActive = FsmStruct;
79 CommFsmDoneFlag = 1; // the FSM is started
80 }
Finite State Machine [13]
La macchina a stati finiti legge tutte le informazioni definite nella struttura ed esegue le operazioni indicate.
82 void CommFsmSched(struct FsmTable * FsmStruct)
83 {//Communication Finite State Machine
84 unsigned char FsmStat;
85
86 FsmStat = FsmStruct[FsmIndx].Stat;
87
88 switch (FsmStat)
89 {
90 case FsmRx:
91 if(RxFsmFlag==0)
92 {// the first time is called
93 StartRx(FsmStruct[FsmIndx].Str);
94 }
95 else
96 {// after terminator string has been received
97 // execute the callback function
98 CommFsmState = (* FsmStruct[FsmIndx].pCallback);
99 (* CommFsmState) () ;
100 FsmIndx++;
101 RxFsmFlag = 0;
102 }
103 break;
104
105 case FsmTx:
106 // transmit the string and then call the callback function
107
108 if(TxFsmFlag==0)
109 {// the first time is called
110 StartTx(FsmStruct[FsmIndx].Str);
111 }
112 else
113 {// after all the buffer is transmitted
114 // execute the final function
115 CommFsmState = (* FsmStruct[FsmIndx].pCallback);
116 (* CommFsmState) () ;
117 FsmIndx++;
118 TxFsmFlag = 0;
119 }
120
121 break;
122
123 case FsmDo:
124 CommFsmState = (* FsmStruct[FsmIndx].pCallback);
125 (* CommFsmState) () ;
126 FsmIndx++;
127 break;
128
129
130 case FsmEnd:
131 CommFsmState = (* FsmStruct[FsmIndx].pCallback);
132 (* CommFsmState) () ;
133 CommFsmFlag = 0;
134 CommFsmDoneFlag = 0; // the FSM is over
135 break;
136
137 default:
138 CommFsmFlag = 0;
139 break;
140 }
141 }
L'intero progetto MPLABX C per il PIC18F4620 è disponibile come open source in GitHub repository [7]
Scansione della Matrice. Procedure e tecniche.
Per pilotare la matrice abbiamo bisogno di 10 porte I/O del PIC18F4620 per le righe e 12 per le colonne, cioè 22 pin occupati dei 40 disponibili in questa versione della MCU.
Le righe, da 0 a 9, sono selezionate una dopo l'altra con periodo di 1 ms, con un periodo complessivo di 10ms. Il frame rate di 100Hz è adatto ad avere una visualizzazione libera da sfarfallio.
Le colonne, da A a L, sono selezionate in parallelo quando la riga corrispondente deve essere illuminata.
1006 typedef union {
1007 struct {
1008 unsigned LATA0 :1;
1009 unsigned LATA1 :1;
1010 unsigned LATA2 :1;
1011 unsigned LATA3 :1;
1012 unsigned LATA4 :1;
1013 unsigned LATA5 :1;
1014 unsigned LATA6 :1;
1015 unsigned LATA7 :1;
1016 };
1017 ......
1048 } LATAbits_t;
1049 extern volatile LATAbits_t LATAbits @ 0xF89;
Qui un esempio del file di intestazione del PIC che mostra come le porte sono mnemonicamente definite. L'esempio è per PORT A.
Mnemonici [14]
La maggior parte degli I/O di tutte le porte sono utilizzati per pilotare le righe e le colonne. Per avere una denominazione più mnemonica ho ulteriormente definito le porte. Oltre alle definizioni per le righe.
56 #define Row0 LATAbits.LATA1
57 #define Row1 LATAbits.LATA2
58 #define Row2 LATAbits.LATA3
59 #define Row3 LATAbits.LATA4
60 #define Row4 LATAbits.LATA5
61 #define Row5 LATCbits.LATC0
62 #define Row6 LATCbits.LATC1
63 #define Row7 LATBbits.LATB1
64 #define Row8 LATBbits.LATB2
65 #define Row9 LATBbits.LATB3
Righe [15]
Le righe sono pilotate in modo sequenziale, una a una ogni 1 ms, indirizzate tramite gli mnemonici definiti.
32 void SetRow(unsigned char Row)
33 {// from 0 to 9, enable one row at a time to scan matrix
34 switch (Row)
35 {
36 case 0:
37 Row0 = ON;
38 break;
39
40 case 1:
41 Row1 = ON;
42 break;
43
44 case 2:
45 Row2 = ON;
46 break;
47
48 case 3:
49 Row3 = ON;
50 break;
51
52 case 4:
53 Row4 = ON;
54 break;
55
56 case 5:
57 Row5 = ON;
58 break;
59
60 case 6:
61 Row6 = ON;
62 break;
63
64 case 7:
65 Row7 = ON;
66 break;
67
68 case 8:
69 Row8 = ON;
70 break;
71
72 case 9:
73 Row9 = ON;
74 break;
75
76 default:
77 SetRowOff();
78 break;
79 }
80 }
Per le colonne è un po' più complicato. Tutte e 12 devono essere impostate nel ciclo di 1 ms. Al fine di risparmiare tempo prezioso sono stati utilizzati alcuni trucchi.
Definizione delle parole sulla matrice [16]
La matrice è stata definita con un array in cui l'indice definisce la variabile a 16 bit corrispondente a ciascuna riga e i bit di ciascuna variabile definiscono la singola colonna dei LED, come riportato in questo foglio [17], e traslata nel codice con la procedura WordSetting () .
170 void WordSetting()
171 {// set the matrix with the words according to the new time
172 int i;
173
174 for(i=0; i<=MAXROW; i++)
175 {// reset the matrix
176 Matrix[i]=0;
177 }
178
179 if(Min <= 30)
180 {// after half hour the word is "to" the next hour, before is "after"
181 Matrix[6]=0b000000011110; // E
182 }
183 else
184 {
185 Hour++;
186 Matrix[7]=0b100000000000; // MENO
187 }
188
189 switch(Min / 5)
190 {
191 case 1:
192 case 11:
193 Matrix[8] = Matrix[8] | 0b000001111110;// CINQUE
194 break;
195
196 case 2:
197 case 10:
198 Matrix[9] = Matrix[9] | 0b111100000000;// DIECI
199 break;
200
201 case 3:
202 case 9:
203 Matrix[7] = Matrix[7] | 0b001101111110;// UN QUARTO
204 break;
205
206 case 4:
207 case 8:
208 Matrix[8] = Matrix[8] | 0b111110000000;// VENTI
209 break;
210
211 case 5:
212 case 7:
213 Matrix[8] = Matrix[8] | 0b111111111110;// VENTICINQUE
214 break;
215
216 case 6:
217 Matrix[9] = Matrix[9] | 0b000000111110;// MEZZA
218 break;
219
220 default:
221 break;
222 }
223
224 if(!TimeSync)// light up points only if RTC time is in sync with INRIM
225 {
226 switch(Min % 5)
227 {
228 case 1:
229 Matrix[0] = Matrix[0] | 0b000000000001;// first point
230 break;
231
232 case 2:
233 Matrix[0] = Matrix[0] | 0b000000000001;// first point
234 Matrix[1] = Matrix[1] | 0b000000000001;// add second point
235 break;
236
237 case 3:
238 Matrix[0] = Matrix[0] | 0b000000000001;// first point
239 Matrix[1] = Matrix[1] | 0b000000000001;// add second point
240 Matrix[2] = Matrix[2] | 0b000000000001;// add third point
241 break;
242
243 case 4:
244 Matrix[0] = Matrix[0] | 0b000000000001;// first point
245 Matrix[1] = Matrix[1] | 0b000000000001;// add second point
246 Matrix[2] = Matrix[2] | 0b000000000001;// add third point
247 Matrix[3] = Matrix[3] | 0b000000000001;// add fourth point
248 break;
249
250 default:
251 break;
252 }
253 }
254
255 Hour = Hour % 12;
256
257 if(Hour == 1)
258 {
259 Matrix[1] = Matrix[1] | 0b101000000000;// E' L'
260 }
261 else
262 {
263 Matrix[0] = Matrix[0] | 0b111101101110;// SONO LE ORE
264 }
265
266 switch(Hour)
267 {
268 case 1:
269 Matrix[1] = Matrix[1] | 0b000111000000;// UNA
270 break;
271
272 case 2:
273 Matrix[1] = Matrix[1] | 0b000000011000;// DUE
274 break;
275
276 case 3:
277 Matrix[2] = Matrix[2] | 0b111000000000;// TRE
278 break;
279
280 case 4:
281 Matrix[5] = Matrix[5] | 0b111111100000;// QUATTRO
282 break;
283
284 case 5:
285 Matrix[6] = Matrix[6] | 0b111111000000;// CINQUE
286 break;
287
288 case 6:
289 Matrix[5] = Matrix[5] | 0b000000001110;// SEI
290 break;
291
292 case 7:
293 Matrix[4] = Matrix[4] | 0b000000111110;// SETTE
294 break;
295
296 case 8:
297 Matrix[2] = Matrix[2] | 0b000111100000;// OTTO
298 break;
299
300 case 9:
301 Matrix[2] = Matrix[2] | 0b000000011110;// NOVE
302 break;
303
304 case 10:
305 Matrix[3] = Matrix[3] | 0b111110000000;// DIECI
306 break;
307
308 case 11:
309 Matrix[3] = Matrix[3] | 0b000001111110;// UNDICI
310 break;
311
312 case 0:
313 Matrix[4] = Matrix[4] | 0b111111000000;// DODICI
314 break;
315
316 default:
317 break;
318 }
319
320 SetColB();
321 }
Pre-setting per le colonne [18]
La traslazione (verso sinistra) sarebbe troppo pesante per essere eseguita ogni 1 ms, perché richiederebbe molte operazioni di mascheratura per impostare ciascuna porta.
Per questo motivo viene eseguita solo ad ogni cambio di visualizzazione, cioè ogni minuto e il risultato è memorizzato nel buffer in una matrice di int senza segno. Questo spreca più memoria (un int per ogni bit), ma permette un grosso risparmio di tempo.
82 void SetColB(void)
83 {// pre-fill the matrix with bytes instead of bits.
84 // This wastes memory but saves a lot of time during runtime matrix scan
85 char Col;
86 char Row;
87 unsigned int Mask;
88
89 for(Row = MINROW; Row <= MAXROW; Row++)
90 {
91 for(Col = MinCol; Col <= MaxCol; Col++)
92 {
93 Mask=BitMask[Col] & Matrix[Row];
94
95 if(Mask)
96 {
97 MatrixB[Row][MaxCol-Col] = ON;
98 }
99 else
100 {
101 MatrixB[Row][MaxCol-Col] = OFF;
102 }
103 }
104 }
105 }
Impostazione dinamica delle colonne [19]
Nel modo descritto in precedenza la conversione viene eseguita in 36μS invece di centinaia di μS. In questo modo si spreca solo una piccola percentuale del periodo di 1 ms, consentendo la regolazione dei LED attraverso la riduzione del duty cycle.
107 void SetCol(unsigned char Row)
108 {/* from 'A' to 'L', enable all the column to light all the LEDs interested
109 the byte matrix is pre-computed when LEDs to display change (every minute)
110 this saves a lot of time during 1ms column scan
111
112 * RD0 = Column A
113 * RD1 = Column B
114 * RD2 = Column C
115 * RD3 = Column D
116 * RD4 = Column E
117 * RD5 = Column F
118 * RD6 = Column G
119 * RD7 = Column H
120 *
121 * RE0 = Column I
122 * RE1 = Column J
123 * RE2 = Column K
124 *
125 * RC5 = Column L
126 */
127
128 ColA = MatrixB[Row][11];
129 ColB = MatrixB[Row][10];
130 ColC = MatrixB[Row][9];
131 ColD = MatrixB[Row][8];
132 ColE = MatrixB[Row][7];
133 ColF = MatrixB[Row][6];
134 ColG = MatrixB[Row][5];
135 ColH = MatrixB[Row][4];
136
137 LATD = Dbits.Port;
138
139 ColI = MatrixB[Row][3];
140 ColJ = MatrixB[Row][2];
141 ColK = MatrixB[Row][1];
142
143 LATE = Ebits.Port;
144
145 ColL = MatrixB[Row][0];
146
147 LATC = PORTC | Cbits.Port;
148 }
Al fine di risparmiare tempo durante le procedure eseguite più spesso, i valori come BitMask [20] e DutyTab [21] sono pre-calcolati all'avvio o in zone meno critiche del programma.
ISR a bassa priorità [22]
La luminosità ambiente viene letta dalla fotocellula attraverso periferiche ADC.
182 // Low priority interrupt vector
183 void interrupt low_priority low_isr (void)
184 {
185 .......
244
245 if (PIR1bits.ADIF)
246 {// A new AD value has been read
247 PIR1bits.ADIF=0; // reset interrupt flag
248 DutyCycle=(ADRESH*LightIndx)>>8;//only the highest 8 bits are read
249 //>>8 to convert back index to int
250 }
251
252 }
Definizione del duty cycle del PWM [23]
Viene poi convertita in tempo con l'aiuto dei valori DutyTab[21].
In questo modo il TMR1 spegne le colonne attenuando la luminosità dei LED secondo il duty cycle calcolato.
160 void SetTimer1(unsigned char DutyCycle)
161 {
162 //set both High and Low registers for Timer1
163 int Count = DutyTab[DutyCycle];
164 TMR1H = Count >> 8; // byte High
165 TMR1L = Count; // byte Low
166 T1CONbits.TMR1ON=1;
167 }
Matrix test [24]
Questa Procedura viene compilata solo quando necessario per testare tutto l'hardware e le funzionalità di matrice.
323 void TestMatrix()
324 {
325 static unsigned char Xcol;
326 static unsigned char Yrow;
327 int i;
328 static int j = 0;
329 static int k = 0;
330
331
332 for(i=MINROW; i<=MAXROW; i++)
333 {// reset the matrix
334 Matrix[i]=0;
335 }
336
337 switch(j)
338 {
339 case 0: // char display
340 WriteMatrixChar('G', 'O', 7, 1);
341 TestTime = 5000;
342 j++;
343 break;
344
345 case 1: // dot by dot
346 Matrix[Yrow]=0b0000100000000000 >> Xcol;
347 if((++Xcol)>MaxCol)
348 {
349 Xcol = MinCol;
350 if((++Yrow)>MAXROW)
351 {
352 Yrow=MINROW;
353 j++;
354 }
355 }
356 TestTime = 100;
357 break;
358
359 case 2: // row by row
360 Matrix[Yrow]=0b0000111111111111;
361 if((++Yrow)>MAXROW)
362 {
363 Yrow=MINROW;
364 j++;
365 }
366 TestTime = 300;
367 break;
368
369 case 3: //column by column
370 Matrix[0]=0b0000100000000000 >> Xcol;
371 Matrix[1]=0b0000100000000000 >> Xcol;
372 Matrix[2]=0b0000100000000000 >> Xcol;
373 Matrix[3]=0b0000100000000000 >> Xcol;
374 Matrix[4]=0b0000100000000000 >> Xcol;
375 Matrix[5]=0b0000100000000000 >> Xcol;
376 Matrix[6]=0b0000100000000000 >> Xcol;
377 Matrix[7]=0b0000100000000000 >> Xcol;
378 Matrix[8]=0b0000100000000000 >> Xcol;
379 Matrix[9]=0b0000100000000000 >> Xcol;
380 if((++Xcol)>MaxCol)
381 {
382 Xcol = MinCol;
383 j++;
384 }
385 TestTime = 300;
386 break;
387
388 case 4: //diagonal left
389 Matrix[0]=0b0000100000000000 >> Xcol;
390 Matrix[1]=0b0000010000000000 >> Xcol;
391 Matrix[2]=0b0000001000000000 >> Xcol;
392 Matrix[3]=0b0000000100000000 >> Xcol;
393 Matrix[4]=0b0000000010000000 >> Xcol;
394 Matrix[5]=0b0000000001000000 >> Xcol;
395 Matrix[6]=0b0000000000100000 >> Xcol;
396 Matrix[7]=0b0000000000010000 >> Xcol;
397 Matrix[8]=0b1000000000001000 >> Xcol;
398 Matrix[9]=0b0100000000000100 >> Xcol;
399 if((++Xcol)>MaxCol)
400 {
401 Xcol = MinCol;
402 j++;
403 }
404 TestTime = 300;
405 break;
406
407 case 5: //diagonal right
408 Matrix[0]=0b0000000000000100 << Xcol;
409 Matrix[1]=0b0000000000001000 << Xcol;
410 Matrix[2]=0b0000000000010000 << Xcol;
411 Matrix[3]=0b0000000000100000 << Xcol;
412 Matrix[4]=0b0000000001000000 << Xcol;
413 Matrix[5]=0b0000000010000000 << Xcol;
414 Matrix[6]=0b0000000100000000 << Xcol;
415 Matrix[7]=0b0000001000000000 << Xcol;
416 Matrix[8]=0b0000010000000001 << Xcol;
417 Matrix[9]=0b0000100000000010 << Xcol;
418 if((++Xcol)>MaxCol)
419 {
420 Xcol = MinCol;
421 j++;
422 }
423 TestTime = 300;
424 break;
425
426 case 6: //all LEDs on
427 if(k%2)
428 {
429 Matrix[0]=0XFFFF;
430 Matrix[1]=0XFFFF;
431 Matrix[2]=0XFFFF;
432 Matrix[3]=0XFFFF;
433 Matrix[4]=0XFFFF;
434 Matrix[5]=0XFFFF;
435 Matrix[6]=0XFFFF;
436 Matrix[7]=0XFFFF;
437 Matrix[8]=0XFFFF;
438 Matrix[9]=0XFFFF;
439 }
440 else
441 {
442 Matrix[0]=0;
443 Matrix[1]=0;
444 Matrix[2]=0;
445 Matrix[3]=0;
446 Matrix[4]=0;
447 Matrix[5]=0;
448 Matrix[6]=0;
449 Matrix[7]=0;
450 Matrix[8]=0;
451 Matrix[9]=0;
452 }
453
454 if((++k)>8)
455 {
456 k = 0;
457 j++;
458 }
459 TestTime = 1000;
460 break;
461
462 default:
463 j=0;
464 TestTime = 300;
465 break;
466 }
467
468 // Matrix[0]=0b0000111111111111;
469
470 SetColB();
471 }
Questa comprende anche la visualizzazione dei caratteri utilizzando un font 7x5 [25].
Con i metodi utilizzati per pilotare la matrice è stato veramente facile usando una delle tante descrizioni di font disponibili in rete. L'unico problema è stato quello di scambiare righe e colonne perché normalmente si usa una scansione a colonne e non per righe. Questa operazione è stata eseguita con questo foglio di lavoro [26].
Un video che mostra come funziona questo test è visibile in [27].
L'intero progetto MPLABX C per il PIC18F4620 è disponibile come open source in GitHub repository [7]
LINKS capitolo Software:
[1] http://ww1.microchip.com/downloads/en/AppNotes/00736a.pdf
[2] https://github.com/guiott/wordclock/blob/master/myi2c.c#L259
[3] https://github.com/guiott/wordclock/blob/master/myi2c.c#L309
[4] https://github.com/guiott/wordclock/blob/master/myi2c.c#L239
[5] https://github.com/guiott/wordclock/blob/master/myi2c.c#L118
[6] https://github.com/guiott/wordclock/blob/master/WordClock.c#L255
[7] https://github.com/guiott/wordclock
[8] http://ww1.microchip.com/downloads/en/DeviceDoc/50002230A.pdf
[9] https://code.google.com/p/wifly-wonderland/wiki/WiFly_comandi_utili
[10] https://github.com/guiott/wordclock/blob/master/comm.h#L33
[11] https://github.com/guiott/wordclock/blob/master/comm.h#L68
[12] https://github.com/guiott/wordclock/blob/master/comm.c#L72
[13] https://github.com/guiott/wordclock/blob/master/comm.c#L82
[14] https://github.com/guiott/wordclock/blob/master/matrix.h#L56
[15] https://github.com/guiott/wordclock/blob/master/matrix.c#L32
[16] https://github.com/guiott/wordclock/blob/master/matrix.c#L170
[17] http://www.guiott.com/WC/WordClock.xlsx
[18] https://github.com/guiott/wordclock/blob/master/matrix.c#L82
[19] https://github.com/guiott/wordclock/blob/master/matrix.c#L107
[20] https://github.com/guiott/wordclock/blob/master/matrix.c#L5
[21] https://github.com/guiott/wordclock/blob/master/matrix.h#L137
[22] https://github.com/guiott/wordclock/blob/master/WordClock.c#L183
[23] https://github.com/guiott/wordclock/blob/master/WordClock.c#L160
[24] https://github.com/guiott/wordclock/blob/master/matrix.c#L323
[25] https://github.com/guiott/wordclock/blob/master/Font8x5.h#L18
[26] http://www.guiott.com/WC/Font5x8.xlsx
[27] https://www.youtube.com/watch?v=IOzSpM-XEm4&feature=youtu.be
Assemblaggio
Collegamenti e millefori
In questa ultima parte del progetto Word Clock, si descrivono i passaggi per l'assemblaggio di tutte le componenti per dare una “veste” all'orologio, inserendolo in una cornice tale da poterlo anche destinare ad ambienti diversi dal laboratorio, nel quale anche le schede a vista sono accettate, o meglio, benvenute...
La figura mostra la scheda finale nella versione prototipale, realizzata utilizzando la pluri-collaudata millefori. La scomodità del wiring è compensata dalla rapidità con cui si possono realizzare dei circuiti senza dover necessariamente realizzare il circuito stampato. Per molti invece, la realizzazione dello stampato stesso rappresenta una fase quasi ludica. Si può realizzare con tecniche casalinghe o con sistemi altamente professionali. Oggi entrambe le possibilità sono alla portata anche dell'hobbista con poche disponibilità economiche. Il circuito stampato si può realizzare in ambiente casalingo con tecniche semplici e con l'”acido”, come “una volta”. Si può poi passare al bromografo (ma anche questo è facile da realizzare in casa con materiale di recupero [1]). Oppure si può realizzare lo sbroglio con uno dei tanti programmi di CAD dedicati agli stampati e poi inviarli ai service specializzati, che oramai anche con spese abbordabili realizzano PCB (Printed Circuit Board) alla portata di tutti.
Per chi fosse interessato alla realizzazione del circuito ecco il layout con la disposizione dei componenti. Anche se non è stato realizzato il PCB, si è usato ugualmente un CAD specifico per ottimizzare la distribuzione dei fili da saldare.
Wiring
Per i collegamenti sono necessari pochi attrezzi, ma più sono professionali, più diventa facile realizzare quello che si vede nelle immagini riportate di seguito.
Un buon filo tipo quello da Wire-Wrap (metodo “antico” che non richiede neanche l'uso del saldatore [2]), aiuta molto. Se si usa il saldatore con fili di scarsa qualità, anche solo avvicinandolo, si squaglia letteralmente buona parte della protezione del collegamento di rame, con le relative conseguenze di corti nascosti dietro ogni collegamento. Anche una buona “spella fili” e un'affilata tronchesina, aiutano molto. Per il resto, occorre solo tanta pazienza e un po' di metodo, ma anche questo fa parte del viaggio e può avere il suo fascino.
Dalle differenti viste delle immagini precedenti si può facilmente immaginare che occorre un minimo di precisione e metodo per poter arrivare ad un circuito funzionante. I vari “ponticelli”, in altre parole, le sovrapposizioni dei fili, sono molto frequenti, quindi come dicevamo è molto importante che non ci siano parti di filo scoperte, perché il corto è dietro l'angolo. Le saldature devono essere pulite e ben fatte, anche in questo caso per evitare corti tra pin vicini. A meno di quelle volute, come dall'immagine si può notare, che in alcuni casi, invece di usare dei fili spellati per collegamenti adiacenti, si usa estendere la saldatura anche su più fori. Anche qui, una buona scheda millefori, aiuta, rispetto ad una molto economica, dove le piazzole non ben definite o ben fatte portano a continui sollevamenti delle piazzole stesse (come facilmente accade se si insiste troppo col saldatore).
Dalle varie immagini si può notare che tutti gli integrati sono montati con zoccoli (o con l'equivalente di Strip femmina, a passo 2,54, sul tipo di questo [3]). Questo per due motivi, sia per poter sostituire facilmente l'eventuale componente che si guasta, ma, soprattutto, per evitare di portare direttamente il saldatore su componenti un po' più “sensibili” alla temperatura. In ogni caso come per le piazzole, ma, soprattutto, per i componenti, è importante avere una buona tecnica nel saldare. Per non bruciare piazzole o componenti o, al contrario per non fare “saldature fredde”, cioè quelle saldature fatte o troppo in fretta o su superfici non ben pulite che comportano facili falsi contatti o, nel caso peggiore, sconnessioni. Anche soffiare sulla saldatura, attività comune, sarebbe non consigliato, perché aumenta la possibilità che lo stagno non si distribuisca bene o non si raffreddi correttamente.
Una volta realizzato il circuito e debitamente testato, si passa, sempre per chi vuole cimentarsi, alla realizzazione del contenitore.
In questo caso le scelte sono veramente infinite, di seguito vediamo una delle possibili soluzioni. Anche qui, come per il circuito stampato, per molti, come me, la realizzazione ha la sua parte di fascino.
Pannello
In questa parte dell'articolo si descrivono in modo prevalentemente “pittorico” le varie fasi per la realizzazione del contenitore.
Partiamo dal pannello che ospiterà i LED e dalla maschera necessaria per rappresentare i caratteri. Il tutto con una buona visibilità, a seguito di una buona diffusione della luce bilanciando una sufficiente luminosità con una buona “leggibilità” dei caratteri.
I componenti di base utilizzati per realizzare il pannello per la matrice di LED, sono:
- Due fogli 20x20cm di PVC espanso,
- la maschera delle lettere,
- e alcuni fogli di diffusione.
Una stampa dalla griglia per le lettere è utilizzata come maschera di foratura per segnare i punti da perforare. Righello e matita, oppure altre forme più “moderne”, a discrezione personale.
Incidendo con un perno, usato come guida per aiutare la successiva foratura.
Guidando il trapano con un puntatore laser, aumenta la precisione del trapano stesso. Ottenendo un pattern, un modello, molto regolare, come mostrato nelle figure seguenti.
Anche in questo caso, come si può notare dalle immagini che seguono, il puntatore laser è rigorosamente home-made. Da buon maker ovviamente si adottano tecniche di “hackeraggio” di altri attrezzi o dispositivi più o meno comuni.
Sempre con la dovuta attenzione, nell'usare dispositivi che possano danneggiare la vista o altro.
Lo stesso vale per tutti gli attrezzi mostrati in questo articolo. Si declina ogni responsabilità per un uso improprio degli stessi, sia per eventuali danni a cose o a persone.
Ovviamente se si dispone di un buon trapano a colonna o le attrezzature riportate nelle immagini, il tutto diventa “un gioco da ragazzi”. Senza supporto a colonna o fissaggi adeguati, risulta in generale molto più complesso. Come si diceva, buoni strumenti, consentono una buona realizzazione in tempi minori. Ma, non è detto che con la manualità non si riesca a sopperire quello che gli attrezzi sono in grado di fornire.
Il laboratorio non nasce necessariamente già tutto attrezzato, ma lo può diventare piano piano a seconda delle disponibilità e delle intenzioni personali.
Ovviamente, neanche a dirlo, se si ha la possibilità di avere nelle vicinanze un buon laboratorio di un amico o, ancora meglio, un buon FabLab attrezzato...(un buon esempio fra tutti a Roma [4]).
Proseguiamo con le forature...
Nell'immagine sono rappresentati i fori da 5mm, adatti per alloggiare i LED nel pannello.
I LED (110 per la matrice + 4 agli angoli per i conteggi parziali) montati sul panello posteriore.
I LED sono levigati in punta, per ottenere una migliore diffusione della luce, altrimenti molto concentrata.
Il Pannello frontale con i fori più grandi che agiscono come maschera.
Il Pannello frontale (grossi buchi = mascherina per la luce) montato sulla parte superiore del pannello posteriore con i LED.
Assemblaggio Finale
E' stata utilizzata una cornice 25x25cm commerciale, con sufficiente profondità per contenere tutto [5].
La maschera per le Lettere.
La maschera per le lettere speculare per migliorare il nero. Raddoppiato e a specchio su carta trasparente. Il lato con l'inchiostro è all'interno per essere protetto dai graffi.
La maschera finale con un diverso font di caratteri (alta risoluzione).
Una vista interna del telaio con la maschera nera per adattare le dimensioni.
Un altro passaggio nell'assemblaggio.
E quindi, l'assemblaggio finale.
Un immagine con la scheda montata sul pannello posteriore.
E quindi il Pannello posteriore con il connettore per la programmazione del PIC (la porta ICSP) ed il GPIO9 del WiFly (Per avviare il WiFly come AP, collegarcisi con un browser qualsiasi e collegarsi ad una rete WiFi esistente).
Per la programmazione del PIC è stato usato il Programmatore ufficiale della MicroChip, l'ICD3, connesso alla porta ICSP, come accennato. Oltre all'ottima qualità come programmatore consente anche il Debugging, molto utile nella maggior parte delle situazioni e per la fase iniziale di avvio del progetto, quando i Bug o le tarature da fare sono molto frequenti.
Una sequenza (con un GIF animato) che mostra come i diversi strati sono messi uno sopra l'altro, a partire dai fogli di diffusione della luce e quindi le diverse maschere con le lettere per ottenere un sufficiente contrasto.
Per finire il pannello in lavorazione installato all'interno del telaio.
E per chiudere un breve video per mostrare il funzionamento reale [6].
LINKS capitolo Assemblaggio:
[1] http://guiott.com/bromografo/bromografo.html
[2] http://en.wikipedia.org/wiki/Wire_wrap
[3] http://www.robot-italy.com/it/female-strip-rounded-0-1-pitch-36pin.html
[4] https://www.fablabs.io/fablabspqwork
[5] http://www.ikea.com/it/it/catalog/products/00078051/
[6] https://www.youtube.com/watch?v=IOzSpM-XEm4&feature=youtu.be
Bel progetto. Vedi interessanti applicazioni a partire da un semplice gioco di luci con la matrice. Da capire se la matrice può essere fatta più grande con opportune modifiche HW e SW.