Programmare in mikroBasic
 
  Box
Subroutines, procedure e funzioni
(prima parte)

Per le informazioni riferite a questo argomento e per compilare i programmi è stata usata la vers. 7.0.0.2 di mikroBasic.
Altre versioni del compilatore potrebbero avere comportamenti diversi o non funzionare.

Quando si scrive un programma, si deve cercare, in tutti i modi, di fargli fare un sacco di cose, ma senza occupare troppa memoria programma (quella che viene anche detta ROM-memory).
Bisogna, soprattutto, cercare di non DUPLICARE alcune operazioni.

Per fare questo, ci vengono in aiuto i sottoprogrammi e le procedure, che sono piccoli blocchi di istruzioni (ma possono essere paragonati a piccoli singoli programmi).
Se, per esempio, dobbiamo mandare dei dati ad un LCD, invece di ripetere, le stesse istruzioni, un sacco di volte, le possiamo mettere in una procedura e chiamare questa, ogni volta che ci servono quelle istruzioni. Se le istruzioni, nel programma, occupavano 30 words e venivano eseguite 10 volte (totale 300 words), per mandare i dati al LCD, chiamando la procedura, adesso il programma occupa 270 words in meno.

Prendiamo, ad esempio, un programma molto semplice (il solito simil-supercar).

program led_danzanti
main:
    trisB = 0
    portB = 0
danza:
    portB = %00011000
    delay_ms(200)
    portB = %00100100
    delay_ms(200)
    portB = %01000010
    delay_ms(250)
    portB = %10000001
    delay_ms(250)
    portB = %10000000
    delay_ms(300)
    portB = %00000001
    delay_ms(300)
    portB = %10000000
    delay_ms(250)
    portB = %00000010
    delay_ms(250)
    portB = %01000000
    delay_ms(200)
    portB = %00000010
    delay_ms(200)
    portB = %00100000
    delay_ms(150)
    portB = %00000100
    delay_ms(150)
    portB = %00100000
    delay_ms(150)
    portB = %00001000
    delay_ms(100)
    portB = %00010000
    delay_ms(100)
    portB = %00001000
    delay_ms(100)
    goto danza
End.

Per questo esempio è stato usato un PIC16F877A con quarzo da 8MHz, collegando i LED su portB.

Partiamo accendendo i 2 LED centrali (portB = %00011000)
e proseguiamo accendendo gli altri, allontanandoci dal centro, fino a quelli esterni (portB = %10000001)
e poi, palleggiando, (portB = %10000000)
ritornare al centro e ricominciare daccapo (goto danza).

Ogni LED, prima di cambiare stato, starà acceso per tutto il tempo definito da un'istruzione di ritardo (delay_ms(...)).

Questa istruzione genera un ritardo di un numero di millisecondi la cui quantità è scritta tra le parentesi tonde [delay_ms(200)].

Notiamo subito che ci sono delle istruzioni uguali che si ripetono [delay_ms(200), delay_ms(100), delay_ms(300), ecc. ecc.].

Se compiliamo il progetto, vedremo che il programma occuperà 532 words nella memoria programma.

   

Cliccando ora su View statistics si aprirà la finestra Statistics.

Adesso possiamo cliccare su Procedures (sizes) e vediamo che l'unica procedura esistente nel programma è la "main" ed occupa 531 locazioni della ROM.

Chiudiamo la finestra Statistics.

Cliccando su "View assembly",

(1) cerchiamo la prima istruzione delay_ms(200)
e notiamo che il codice prodotto per generare il ritardo

(2) è composto di 30 istruzioni assembly
e che la situazione si ripete per tutte le altre istruzioni delay_ms(...) simili.

Questo ci deve far pensare che, anche se in mikroBasic un'istruzione è di una sola riga, in assembly potrebbe essere tradotta in molte righe di codice.

Bastano poche istruzioni di questo tipo in mikroBasic, per far aumentare la dimensione dell'eseguibile finale da inserire nel PIC.

Dovremo, perciò, cercare di ridurre le istruzioni o blocchi di istruzioni uguali duplicate.

Lo faremo, modificando il programma, dopo aver visto cosa sono i sottoprogrammi.

Usiamo i sottoprogrammi

main:
    ......
    (istruzioni)
    ......
Etichetta_1:
    ......
    (istruzioni)
    ......
    gosub etichetta_sub 'salta a subroutine
    ......              'istruz. che verrà eseguita
                        'al ritorno dal gosub
    gosub etichetta_sub 'salta a subroutine
    goto etichetta_1    'istruz. che verrà eseguita
                        'al ritorno dal gosub
	
etichetta_sub:          'inizio subroutine
    ......
    (istruzioni)
    ......
    return              'termine subroutine
End.

Subroutines

Una subroutine è una parte di programma, autonomo ed indipendente (sottoprogramma), che esegue determinate istruzioni all'interno del programma principale.

Viene invocata con la parola chiave GOSUB, seguita dall'etichetta dell'inizio della subroutine (etichetta_sub).

Il programma proseguirà, eseguendo la prima istruzione che si trova alla posizione dell'etichetta (etichetta_sub) o riga successiva.

Quando incontrerà la parola chiave RETURN, ritornerà alla riga di programma successiva al comando GOSUB, dato precedentemente.

program led_danzanti_1

main:
    trisB = 0
    portB = 0
danza:
    portB = %00011000
    gosub pausa200
    portB = %00100100
    gosub pausa200
    portB = %01000010
    gosub pausa250
    portB = %10000001
    gosub pausa250
    portB = %10000000
    gosub pausa300
    portB = %00000001
    gosub pausa300
    portB = %10000000
    gosub pausa250
    portB = %00000010
    gosub pausa250
    portB = %01000000
    gosub pausa200
    portB = %00000010
    gosub pausa200
    portB = %00100000
    gosub pausa150
    portB = %00000100
    gosub pausa150
    portB = %00100000
    gosub pausa150
    portB = %00001000
    gosub pausa100
    portB = %00010000
    gosub pausa100
    portB = %00001000
    gosub pausa100
    goto danza
pausa300:
    delay_ms(300)
    return
pausa250:
    delay_ms(250)
    return
pausa200:
    delay_ms(200)
    return
pausa150:
    delay_ms(150)
    return
pausa100:
    delay_ms(100)
    return
End.

Modifica al programma con uso di subroutines

La modifica al nostro programma, consiste nel sostituire le righe di programma, che invocavano il ritardo delay_ms(...), con altrettante che invocano un Gosub pausa..., eseguendo le subroutines corrispondenti.

Le subroutines sono 5 ed ognuna esegue un'istruzione delay_ms(...) per generare il ritardo voluto.

Nel programma precedente c'erano 16 istruzioni delay_ms(...) ed ora ne troviamo solo 5 per avere lo stesso risultato.

Compiliamo il programma e vediamo che adesso, lo spazio occupato dal programma, è diventato di 249 words.

   

Anche in questo caso, cliccando su View statistics
e poi su Procedures (sizes), vediamo che l'unica procedura esistente nel programma, è la "main".

Se il programma fosse composto da molte altre istruzioni, fino a generare codice assembly superiore a 2000 words, il compilatore genererebbe l'errore "Routine too large" (routine troppo grande).

Ma perchè ci da questo errore, se il PIC16F877A può contenere fino a 8192 words di codice?

Questo errore viene generato perchè, in mikroBasic, una procedura non può superare 2000 words di codice e, in questo caso, "main" è una procedura, la procedura principale del programma.

Le subroutines, invocate con Gosub, sono interne alla procedura "main" e ne fanno parte integrante.

Per non incorrere nell'errore "Routine too large", dovremo individuare, nel nostro programma, dei blocchi di istruzioni da poter essere tolti dal "main" ed essere inseriti in altrettante
procedure.

In questa maniera, il programma viene diviso in varie procedure e si eviterà l'errore menzionato precedentemente (purchè, ogni singola procedura, non superi 2000 words).

L'uso di Goto e Gosub è caldamente sconsigliato perchè non efficiente (usarli solo in casi sporadici).

In alternativa a Goto è consigliabile l'uso di cicli While-Wend oppure Do-Loop.

In alternativa a Gosub è doveroso usare le sub-procedure e le sub-function, molto più efficienti e che rendono la programmazione più "pulita" e modulare.

Procedure e funzioni

sub procedure nome_procedura (lista_parametri)
[dichiarazioni variabili locali ]

  corpo della procedura
end sub

Sub procedure

Una sub-procedura, è un sottoprogramma (blocco autonomo di dichiarazioni) che compie un certo compito basato su un numero di parametri di ingresso.
I parametri sono dei valori che vengono impostati (passati) quando la procedura viene invocata.

Il nome della procedura è un nome scelto arbitrariamente, per ricordarci cosa fanno le istruzioni contenute.
La lista dei parametri è composta da zero o più parametri (detti parametri formali), definiti alla stessa maniera di come si definiscono le variabili (può anche non avere parametri).

Una procedura viene invocata col suo relativo nome, con i parametri da passare (detti parametri attuali) messi nella stessa sequenza dei loro parametri formali corrispondenti.

Per essere parte integrante di un programma, la procedura deve trovarsi, come collocazione nel listato, prima della label "main".
Ma può trovarsi anche in un altro modulo, che dovrà essere incluso nel programma principale.
La regola vale anche quando una procedura chiama un'altra procedura; quest'ultima deve essere definita prima della chiamata.

Nel caso di una procedura senza parametri, significa che esegue operazioni con le variabili globali, ma potrebbe anche non usare variabili. Potrebbe, ad esempio, chiamare un'altra procedura, oppure eseguire qualche istruzione sui registri del PIC o le porte, oppure chiamare una procedura della libreria (ad es. Lcd_init per inizializzare un display LCD alfanumerico).

La procedura, DEVE essere invocata, esattamente, come è stata definita.
Bisogna fare attenzione, anche, al TIPO delle variabili da mandare, come parametri.
Se il TIPO non è quello corrispondente, il compilatore può non generare errori di compilazione, ma ci potrebbero essere risultati imprevisti nell'esecuzione. (Per questo argomento, vedi anche questi appunti.)

program prova_proc

dim banana as byte
dim limone as byte

Sub procedure gelato(dim crema, panna as byte)
    dim arancia as byte

    arancia = crema + panna
    banana = panna + arancia
end sub

sub procedure sorbetto
    banana = limone + 45
end sub

main:
    trisB = 0

    While true
        gelato(2, 5)
	
        limone = arancia 'genera ERRORE
	
        limone = banana - 6
        banana = 4
        gelato(limone, banana)

        limone = banana

        sorbetto

        portB = banana
    Wend

End.

Esempio di sub-procedure

Banana e limone sono variabili globali, cioè sono riconosciute da tutto il programma.

La procedura gelato ha 2 parametri formali (crema, panna) di tipo byte.

Arancia è una variab. locale, cioè riconosciuta solo dentro la procedura (se viene usata fuori dalla procedura, genera un errore del compilatore).
Anche i parametri formali sono, in pratica, riconosciuti come variabili locali (crema + panna).

La procedura sorbetto è una procedura senza parametri (esegue operazioni con le variabili globali).

Nella main, invoco la procedura gelato, passandogli 2 valori (2, 5) detti parametri attuali. Questi ultimi possono essere dei valori costanti oppure delle variabili (globali o locali).
I parametri attuali vanno a sostituire i parametri formali della procedura invocata.
In pratica è come se avessi scritto crema = 2 e panna = 5. L'esecuzione del programma "salta" dentro la procedura ed esegue le istruzioni contenute (arancia = 2 + 5) (banana = 5 + 7).
Quando la procedura termina, si ritorna alla riga di programma successiva alla chiamata, in questo caso alla riga:

limone = arancia che genera un ERRORE del compilatore, perchè arancia è una variabile locale definita dentro la procedura gelato. Possiamo cancellare la riga (o mettere un apostrofo per trasformarla in un commento) per proseguire.

limone = banana - 6 e cioè limone = 12 - 6 = 6

Chiamo la procedura gelato passando 2 variabili come parametri attuali, che corrispondono a
crema = limone = 6 e panna = banana = 4.
Al ritorno banana = 14.

Quindi limone = banana = 14

Chiamo la procedura sorbetto senza parametri.
Al ritorno banana = 59 e quindi portB = 59.

Ripeto tutto dall'inizio.

Ritornando al nostro programmino, quella che io ho chiamato istruzione delay_ms(...) è in realtà una procedura contenuta nelle librerie di mikroBasic ed è definita così:

   

Notiamo che l'unico parametro definito è una costante di tipo word. Questo significa che non possiamo passare come parametro delle variabili, ma soltanto valori fissi (costanti). Oltre a ciò, è dichiarata come procedura "inline" (perchè, in realtà, si tratta di una macro). Questo significa che, per ogni riga di programma dove invochiamo questa procedura, verrà generato il codice assembly corrispondente (come abbiamo visto nella prima versione del prog.).

N.B.
Quando definiamo una nostra procedura non possiamo usare costanti nella dichiarazione dei parametri.

Clock MHz Valore max ms
2 100009
4 50004
8 25002
20 10000
40 5000

Misteri di mikroBasic

Esiste una limitazione sul valore che si può immettere, come parametro, per delay_ms. A seconda della frequenza di clock, non si può immettere un valore superiore a quello in tabella, altrimenti ci sarà un messaggio di errore del compilatore.

Nella seconda versione del nostro programma, abbiamo ridotto a 5 le chiamate alla procedura usando le subroutines.
Proviamo ad usare le procedure al posto delle subroutines e vediamo le differenze.

program led_danzanti_2

sub procedure pausa300
    delay_ms(300)
end sub
sub procedure pausa250
    delay_ms(250)
end sub
sub procedure pausa200
    delay_ms(200)
end sub
sub procedure pausa150
    delay_ms(150)
end sub
sub procedure pausa100
    delay_ms(100)
end sub

main:
    trisB = 0
    portB = 0
    While true
        portB = %00011000
        pausa200
        portB = %00100100
        pausa200
        portB = %01000010
        pausa250
        portB = %10000001
        pausa250
        portB = %10000000
        pausa300
        portB = %00000001
        pausa300
        portB = %10000000
        pausa250
        portB = %00000010
        pausa250
        portB = %01000000
        pausa200
        portB = %00000010
        pausa200
        portB = %00100000
        pausa150
        portB = %00000100
        pausa150
        portB = %00100000
        pausa150
        portB = %00001000
        pausa100
        portB = %00010000
        pausa100
        portB = %00001000
        pausa100
    Wend
End.

Programma con uso di procedure

La modifica al nostro programma, consiste nel sostituire le 5 subroutines, del programma precedente, con altrettante procedure.

Definiamo le 5 procedure posizionandole prima dell'etichetta main (obbligatorio).

Esaminiamo la prima (le altre sono simili) a cui abbiamo dato il nome "pausa300". E' una procedura senza parametri la quale chiama la procedura della libreria di mikroBasic delay_ms(...) passando, come parametro (dentro le parentesi), 300 che sono i millisecondi di ritardo voluti.

Le altre procedure sono uguali, ma con parametri per ritardi differenti.

Quando il programma viene eseguito, ogni volta che incontra la riga con l'istruzione pausa300, l'esecuzione proseguirà dentro alla procedura pausa300 ed eseguirà le istruzioni contenute all'interno di essa.
In questo caso, l'unica istruzione presente richiede l'esecuzione di un'altra procedura, delay_ms(300) per creare un ritardo di 300 millisecondi, dopodichè la procedura termina e l'esecuzione proseguirà dall'istruzione successiva a quella della chiamata precedente.

Le altre procedure funzionano alla stessa maniera, generando ritardi differenti.

Compiliamo il programma e vediamo che adesso, lo spazio occupato dal programma, è diventato di 225 words.

   

Cliccando su View statistics e poi su Procedures (sizes), vediamo che il programma è stato diviso in 6 procedure (le nostre 5 procedure più la procedura principale main).

In questa maniera si evita che esca il messaggio d'errore "routine too large", menzionata precedentemente.

   

Abbiamo visto che la procedura delay_ms(...) accetta solo parametri di valore costante. E se avessimo bisogno di passare dei parametri con valori variabili, come facciamo?
Nelle librerie di mikroBasic c'è un'altra procedura con nome Vdelay_ms(...) che fa al caso nostro.

   

Notiamo che anche questa procedura richiede un'unico parametro, una variabile di tipo word che indicherà i millisecondi di ritardo voluti.
E' da notare che, questa procedura, è molto meno precisa di quella precedente. Oltre a ciò, questa è una vera procedura (non una macro, come la precedente). Questo significa che verrà generato il codice assembly corrispondente una sola volta, anche se la invochiamo infinite volte nel programma.
Proviamo a vedere le differenze con il programmino della prima versione modificato.

program led_danzanti_3
main:
    trisB = 0
    portB = 0
    while true
        portB = %00011000
        Vdelay_ms(200)
        portB = %00100100
        Vdelay_ms(200)
        portB = %01000010
        Vdelay_ms(250)
        portB = %10000001
        Vdelay_ms(250)
        portB = %10000000
        Vdelay_ms(300)
        portB = %00000001
        Vdelay_ms(300)
        portB = %10000000
        Vdelay_ms(250)
        portB = %00000010
        Vdelay_ms(250)
        portB = %01000000
        Vdelay_ms(200)
        portB = %00000010
        Vdelay_ms(200)
        portB = %00100000
        Vdelay_ms(150)
        portB = %00000100
        Vdelay_ms(150)
        portB = %00100000
        Vdelay_ms(150)
        portB = %00001000
        Vdelay_ms(100)
        portB = %00010000
        Vdelay_ms(100)
        portB = %00001000
        Vdelay_ms(100)
    wend
End.

Modifica al programma con uso della procedura Vdelay_ms(...)

La modifica al programma, consiste nel sostituire tutte le chiamate alla procedura delay_ms(...) con chiamate alla procedura Vdelay_ms(...).

E' bene sostituire anche l'etichetta danza: e l'istruzione goto danza, con l'istruzione While true e Wend.

Compiliamo il programma e vediamo che adesso, lo spazio occupato dal programma, è diventato di 297 words.
Abbiamo peggiorato rispetto a prima.

   

Cliccando su View statistics e poi su Procedures (sizes), vediamo che il programma è stato diviso in 3 procedure (la procedura Vdelay_ms, la funzione Mul_32x32_U (chiamata necessariamente da Vdelay_ms) e la procedura principale main).

Notiamo quindi che, nonostante abbiamo fatto numerose chiamate alla Vdelay_ms, il compilatore ha generato assembly solo per una copia della procedura.

Di negativo c'è che, per far funzionare detta procedura, il compilatore ha dovuto prendere dalle librerie ed inserire nel codice, anche la funzione Mul_32x32_U necessaria per il calcolo dei tempi.

   

Adesso vediamo un'altra modifica al nostro programmino, usando però una sola procedura delay_ms, usando un trucchetto per avere tempi variabili.

program led_danzanti_4

sub procedure pausa_ms(dim ms as word)
    dim i as word
    for i = 1 to ms
        delay_ms(1)
    next i
end sub

main:
    trisB = 0
    portB = 0
    while true
        portB = %00011000
        pausa_ms(200)
        portB = %00100100
        pausa_ms(200)
        portB = %01000010
        pausa_ms(250)
        portB = %10000001
        pausa_ms(250)
        portB = %10000000
        pausa_ms(300)
        portB = %00000001
        pausa_ms(300)
        portB = %10000000
        pausa_ms(250)
        portB = %00000010
        pausa_ms(250)
        portB = %01000000
        pausa_ms(200)
        portB = %00000010
        pausa_ms(200)
        portB = %00100000
        pausa_ms(150)
        portB = %00000100
        pausa_ms(150)
        portB = %00100000
        pausa_ms(150)
        portB = %00001000
        pausa_ms(100)
        portB = %00010000
        pausa_ms(100)
        portB = %00001000
        pausa_ms(100)
    wend
End.

Modifica al programma con uso di una sola procedura delay_ms

La modifica al nostro programma, consiste nel creare una nostra procedura di nome pausa_ms, con un parametro formale di tipo word, al quale passeremo i valori di tempo voluti.

All'interno della procedura, definiamo una variabile locale i di tipo word, necessaria per il ciclo for.

All'interno del ciclo for invochiamo una chiamata alla procedura delay_ms(1) con il parametro impostato ad 1, che genererà un ritardo di 1 millisecondo.
Il ciclo for eseguirà un numero di cicli che va da 1 al valore contenuto nel parametro ms, il quale viene passato quando invochiamo la procedura pausa_ms e che indica quanti millisecondi vogliamo.

Se invochiamo la procedura, ad esempio, scrivendo pause_ms(300), passiamo alla variabile ms, contenuta nel parametro, il valore 300. Il ciclo for eseguirà quindi 300 cicli consecutivi, ognuno con ritardo di 1 millisecondo, perciò un totale di 300 millisecondi (circa).

Compiliamo il programma e vediamo che adesso, lo spazio occupato dal programma, è diventato di 147 words.
Abbiamo avuto un ulteriore miglioramento rispetto ai precedenti e credo che possiamo fermarci qua.

   

Cliccando su View statistics e poi su Procedures (sizes), vediamo che il programma è stato diviso in 2 procedure (la procedura pausa_ms e la procedura principale main).

   

Schema elettrico

   

Bibliografia:
Manuale mikroBasic

Continua nella seconda parte

Ultima modifica  

 
Privacy Policy Cookie Policy