Cos'è ElectroYou | Login Iscriviti

ElectroYou - la comunità dei professionisti del mondo elettrico

3
voti

Codice per lunghi ritardi in PIC assembler

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:

  1. 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)
  2. Preparo il ciclo interno ponendo: migliore = valore massimo possibile per questo loop, inizio=1, fine=256 e azzero le altre variabili
  3. 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
  4. Calcolo il nuovo valore di indice come: indice = (inizio+fine)\2, valore mediano iniziale; in questo caso pari a 128
  5. Eseguo la chiamata alla routine CalcolaCicli che ritorna in ritardoCalcolato il numero di cicli
  6. Calcolo lo scarto = ritardoCalcolato - ritardoVoluto
  7. Se scarto é negativo significa che il valore di indice é troppo basso (il valore cercato é nella meta' superiore del range
  8. Assegno quindi il valore di indice a inizio per spostare il range in alto e proseguo la ricerca
  9. Se scarto é positivo (o nullo), verifico se esso sia anche minore del valore migliore attuale
  10. Se non é minore, assegno a fine il valore di indice, perché il valore calcolato si pone nella meta' inferiore del range e proseguo
  11. Se invece é minore, assegno a migliore il valore di scarto e memorizzo il valore di indice in SalvaIndice, poi proseguo come da punto precedente
  12. 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

Tools_2_small.jpg

Tools_2_small.jpg

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.

2

Commenti e note

Inserisci un commento

di ,

Hai perfettamente ragione sul 'malfatto', se ri riferisci alla modalita' di generazione di un ritardo. Per certo, l'uso dei timer con conteggio dei cicli di interrupt generati, oppure l'uso del WDT sono soluzioni più eleganti. Sul fatto dell'inutile, non sono del tutto daccordo. Svariate volte ho letto domande in Google di programmatori, certamente alle prime armi, che chiedevano come codificare ritardi di alcuni secondi, a volte anche di pochi minuti. La cosa mi ha incuriosito e ho iniziato a cercare soluzioni 'semplici' per questo tipo di problemi. Per 'semplici' intendo senza l'uso di Interrupt, che all'inizio possono essere ostici. Non ho trovato risposte (tranne il citato tool su PicList) e da qui é nata la sfida a sviluppare la soluzione che, ripeto, non sara' sicuramente l'unica; questa pero' é documentata. Inoltre, ci possono essere casi per i quali una 'banale' routine di ritardo da usare magari solo una volta all'inizio del programma é la soluzione piu' comoda, senza dover programmare interupt da usare o da ripetere una sola volta per poi disabilitare il tutto (con conseguente uso di flag di ridirezione). Il fatto poi di aver esteso il conteggio fino a parecchi giorni é evidentemente un'estremizzazione per capire quali sono i limiti del codice (in questo caso l'ampiezza delle variabili Currency di Visual Basic). Non mi permetterai mai di suggerire un ritardo di una settimana su un Pic, mentre un periodo di 1 o 2 minuti puo' avere uno scopo (es. assestamento della temperatura di un sensore all'avvio programma). In sintesi, al di la' della presunta utilità o inutilità di questo codice, la motivazione di fondo per la sua pubblicazione é didattica e, per me, é stata una sfida intellettuale. Mi assumo la responsabilita' per non aver chiarito questo aspetto in apertura di articolo e provvedero' a porre rimedio. Grazie per la segnalazione e ciao Giacomo

Rispondi

di ,

Interessante ma, secondo me, mal fatto ed inutile. Non tanto il tuo lavoro, ma il fatto di tenere un microcontrollore a contare dei loop per passare il tempo è un modo, generalmente, sbagliato di programmare. Preferisco vedere su un microcontrollore un base tempi e dei timer da costruire ed interrogare, per fare cosa occorre sull'evento del tempo trascorso.

Rispondi

Inserisci un commento

Per inserire commenti è necessario iscriversi ad ElectroYou. Se sei già iscritto, effettua il login.