Indice |
Sommario
Viene illustrato un algoritmo per codificare routine di ritardo medio-lunghe (da millisecondi a giorni) in Assembler per qualsiasi famiglia di PIC a 8 bit e per qualsiasi frequenza di clock, incluso valori frazionari.
NOTA BENE: tra i vari metodi per generare ritardi in assembler, questo é il meno consigliato in quanto non efficiente per l'uso della CPU; sono preferibili metodi basati sui Timer o sul WDT (Watch Dog Timer).
Pur tuttavia, in particolari situazioni questo metodo fornisce una soluzione semplice perché non utilizza Interrupt.
Inoltre, questa presentazione puo' avere una valenza didattica e ha, comunque, rappresentato per me una sfida intellettuale interessante.
Introduzione
Una delle situazioni piu' ostiche che ci si trova ad affrontare nello scrivere codice Assembler per i PIC é la generazione di cicli di ritardo medio-lunghi.
Questo é ancor piu' evidente quando il clock della CPU non é ancora definito in via definitiva o, peggio ancora, quando il clock non é un bel numero intero divisibile per due (es. 13.456MHz).
Il linguaggio C ha il vantaggio di avere funzioni built-in per generare ritardi variabili, seppur limitati nella durata, mentre in Assembler bisogna cavarsela da soli.
La soluzioni piu' comune, adottata anche qui, é di nidificare dei cicli di loop, ciascuno dei quali decrementa una variabile finche questa raggiunge il valore di 0. Ci sono 2 varianti a questa costruzione:
- la prima é di assegnare un valore iniziale alle variabili di loop (d'ora in avanti chiamate 'Contatori') e proseguire con il valore di 0 (equivalente al valore 256) per le successive ripetizioni.
- la seconda é di ri-assegnare il valore di ripetizione (d'ora in avanti chiamate 'Ripetizioni') all'inizio di ciascun ciclo, escluso quello piu' esterno per evitare l'infinito.
Io ho scelto quest'ultima soluzione perché dalle mie sperimentazioni (magari fallaci) ho riscontrato una maggiore approssimazione dei tempi calcolati al valore ricercato.
Sul WEB si trovano un buon numero di soluzioni e algoritmi per creare questi loop e determinare che ritardo viene generato al variare delle variabili di conteggio.
La PRIMA PARTE che segue é la mia soluzione.
Non é invece documentata (o almeno io non sono riuscito a trovarla) la soluzione opposta, ovvero dato un ritardo trovare i valori dei contatori.
In effetti ho trovato un solo tool on-line dell'ottimo Nikolai Golovchenko su http://www.piclist.com che consente questo calcolo, ma senza spiegare come funziona.
La SECONDA PARTE vuole essere il mio contributo a questa, presunta, mancanza.
PRIMA PARTE: dai valori al ritardo
La struttura dei cicli é banale; si parte dal ciclo piu' interno ('Loop1') composto da 4 istruzioni:
Loop1: movlw Ripetizioni1 movwf Contatore1 Loop0: decfsz Contatore1,f goto Loop0
E' evidente che le 2 istruzioni che compongono il sotto-ciclo 'Loop0' sono ripetute per Ripetizioni1-1 volte; quando Ripetizioni1 arriva a 0 viene eseguita una sola istruzione (decfsz) e poi si passa all'istruzione successiva.
Le 2 istruzioni di Loop0 impiegano 3 cicli di clock (ovvero 1+2); l'ultimo decremento impiega invece solo 2 cicli.
La formula risultante per cicli1 (variabile del totale dei cicli impiegati per il Loop1) é quindi:
cicli1 = (Ripetizioni1-1) * cicli0 + (cicli0-1) + 2
dove il 2 finale sono i cicli per le 2 istruzioni di assegnazione valore alla variabile Contatore1.
E' da notare che, come detto, cicli0 é un valore costante pari a 3, per cui cicli0-1 vale 2; in definitiva:
cicli1 = (Ripetizioni1-1) * cicli0 + 4 'qui lascio cicli0 come valore simbolico per rendere piu' chiare le formule successive.
Inserendo poi un secondo Loop, il codice diventa:
Loop2: movlw Ripetizioni2 movwf Contatore2 Loop1: movlw Ripetizioni1 movwf Contatore1 Loop0: decfsz Contatore1,f goto Loop0 decfsz Contatore2,f goto Loop11
e la formula relativa vale:
cicli2 = (Ripetizioni2-1) * (cicli1+3) + cicli1 + 2 + 2
dove:
- (cicli1+3): perché alle 'Ripetizioni2-1' del Loop1 vanno sommati i 3 cicli di decremento di Contatore2
- cicli1 + 2: perché all'ultima ripetizione si sommano solo i 2 cicli di uscita
- +2: finale dato dalle consuete istruzioni di assegnazione iniziale alla variabile contatore.
Si puo' proseguire cosi' per quanti Loop si vuole. Attenzione che con 5 Loop nidificati si arriva all'enorme numero di cicli totali di ben 3,380,982,724,376 che con un clock a 4MHz significano 939 ore (ovvero 39 giorni, abbastanza da far bruciare prima il PIC per la noia)
Riassumo qui le formule, ulteriormente pulite dopo alcune semplificazioni, per 5 (+1) Loop:
cicli0 = 3 cicli1 = Ripetizioni1 * cicli0 + 1 cicli2 = Ripetizioni2 * (cicli1+3) + 1 cicli3 = Ripetizioni3 * (cicli2+3) + 1 cicli4 = Ripetizioni4 * (cicli3+3) + 1 cicli5 = Ripetizioni5 * (cicli4+3) + 1
Con queste formule e con Excel é immediato calcolarsi il numero di cicli totali variando i valori delle Ripetizioni.
Per completezza, al risultato ottenuto occorre anche aggiungere i 4 cicli dati dalla chiamata ('call') e all'uscita ('return') della routine.
E' ovvio che non é indispensabile utilizzare tuti i 5 Loop.
Dato il ritardo voluto, opportunamente trasformato in numero cicli in funzione del CPU clock, si individua facilmente quanti Loop sono necessari, sapendo a priori quali sono i valori massimi (Max) raggiungibili con N Loop.
Per informazione riporto qui questi valori:
- N=1 - Max = 776
- N=2 - Max = 199,780
- N=3 - Max = 51,189,008
- N=4 - Max = 13,155,574,808
- N=5 - Max = 3,380,982,724,376
SECONDA PARTE: dal ritardo ai valori
Devo ammettere che ci ho sbattuto la testa a lungo per trovare una soluzione e poi implementarla.
Non sono un matematico, per cui verro' smentito, ma le formule sopra fornite possono essere riscritte come una (lunga) unica equazione di primo grado in 5 incognite, ovvero non risolvibile con un approccio algebrico.
Una soluzione teorica sarebbe di calcolare il ritardo assegnando tutti i valori da 0 a 255 a tutte le incognite.
Con i PC attuali questo é fattibile solo fino al massimo di 3 incognite, e gia' cosi' occorre reiterare il calcolo 16,777,216 volte!
Con 5 incognite, loop massimi qui previsti, i cicli sarebbero 1,099,511,627,776 !
... un po' troppi, vero?
L'idea é quindi stata di usare l'algoritmo di Ricerca binaria (link all'articolo) per trovare il miglior valore di ripetizioni all'interno di ciascun ciclo
La ricerca richiede un'iterazione costante su 8 valori (nell'intervallo da 1 a 256) per cui il risultato si ottiene in massimo 8*5, cioé soli 40 calcoli; un bell'incremento di efficienza.
Di seguito riporto il diagramma di flusso per una possibile implementazione del codice di calcolo.
Il valore da approssimare é contenuto in ritardoVoluto, ed é espresso in cicli.
La funzione CalcolaCicli, che utilizza le formula della Prima Parte, ritorna il numero di cicli calcolato in ritardoCalcolato
In una fase preparatoria si stabilisce il numero di Loop richesti, nella variabile numeroLoops (banale, no?)
Vengono definite queste variabili:
- inizio: valore basso del campo di ricerca
- fine: valore alto di ricerca
- indice: valore attuale di ricerca
- migliore: registra il miglior valore ottenuto
- scarto: differenza tra ritardoCalcolato e ritardoRichiesto
- salvaCorrente: registra il valore che ha generato il miglior valore
- LP: contatore del loop attuale, inizialmente posto uguale a numeroLoops
- finito: flag booleano per segnalare la fine del calcolo
- fineCiclo: flag booleano per segnalare la fine del ciclo
L'algoritmo é il seguente, con riferimento alla numerazione riportata in immagine:
- Inizializzo le variabili e calcolo il numeroLoop
avvio il ciclo esterno e verifico che LP sia maggiore di zero, altrimenti ho finito il calcolo (finito=vero) - Preparo il ciclo interno ponendo: migliore = valore massimo possibile per questo loop, inizio=1, fine=256 e azzero le altre variabili
- Verifico se la differenza tra i valori di inizio e fine sia uguale a 1; in questo caso ho esaurito la ricerca binaria e passo al punto 11
- Calcolo il nuovo valore di indice come: indice = (inizio+fine)\2, valore mediano iniziale; in questo caso pari a 128
- Eseguo la chiamata alla routine CalcolaCicli che ritorna in ritardoCalcolato il numero di cicli
- Calcolo lo scarto = ritardoCalcolato - ritardoVoluto
- Se scarto é negativo significa che il valore di indice é troppo basso (il valore cercato é nella meta' superiore del range
- Assegno quindi il valore di indice a inizio per spostare il range in alto e proseguo la ricerca
- Se scarto é positivo (o nullo), verifico se esso sia anche minore del valore migliore attuale
- Se non é minore, assegno a fine il valore di indice, perché il valore calcolato si pone nella meta' inferiore del range e proseguo
- Se invece é minore, assegno a migliore il valore di scarto e memorizzo il valore di indice in SalvaIndice, poi proseguo come da punto precedente
- Copio nella membro .indice del Loop il valore salvato in SalvaIndice; questo é il valore che sara' utilizzato per il calcolo finale. Decremento il contatore di Loop e termino il ciclo interno di ricerca
Conclusione
Come anticipato, non so se questo sia il miglior approccio alla risoluzione dello specifico problema.
Se qualcono fosse a conoscenza di altri algoritmi, saro' ben lieto di studiarli.
La precisione dei risultati non é eccelsa, tipicamente inferiore allo 0.1%, ma non penso che essa incida piu' di tanto su temporizzazioni lunghe.
I risultati sono stati verificati 'dal vivo' tramite la mia piattaforma di sviluppo per PIC e utilizzando il debugger per visualizzare i cicli impiegati, in perfetta armonia con le attese. Questo solo per valori di ritardo di pochi millisecondi in quanto il debugger rallenta moltissimo l'esecuzione del codice.
Per prigrizia, non ho fatto il confronto con un orologio alla mano per tempi piu' lunghi, ma sono molto fiducioso.
Programma
Per chi fosse ulteriormente interessato, rimando al link http://www.boxidee.it/Programmazione/pic_tools.html per scaricare (non appena sara' terminato :-)) il programma in Visual Basic Pic Tools che implementa questo algoritmo e fornisce altre funzionalita' sempre relative alla codifica in assembler dei PIC.
Questo programma fornisce tra l'altro lo scarto di calcolo rispetto all'ideale, per cui diviene semplice determinare un valore inferiore all'obiettivo e aggiungere una seconda routine di correzione.