Programmare in mikroBasic
 
  Box
Subroutines, procedure e funzioni
(seconda 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.

Vedi la prima parte

Adesso vediamo un'altro tipo di sottoprogramma, la
sub-function, detta anche chiamata di funzione.

sub function nome_funzione (lista_parametri) as tipo_dato_ritorno
[ dichiarazioni locali ]
  corpo della funzione 
end sub

Sub function

Una funzione è simile ad una procedura, ma che però ritorna un valore quando viene eseguita, mentre la procedura non lo fa.

Il nome della funzione è un nome scelto arbitrariamente, per ricordarci cosa fanno le istruzioni contenute.
La lista dei parametri è composta da uno o più parametri (detti parametri formali), definiti alla stessa maniera di come si definiscono le variabili.
Il tipo dato di ritorno è il tipo di dato del risultato della funzione, che sarà assegnato alla variabile chiamante.
Per indicare il valore del risultato, nel corpo della funzione bisogna usare la variabile result (automaticamente creata locale da mikroBasic), per assegnare il valore di ritorno di una funzione.

Una funzione 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 funzione deve trovarsi, come collocazione nel listato, prima della label "main" e segue le stesse regole delle procedure.

program prova_funz

dim pere as byte
dim mele as byte
dim ricavo as word

sub function somma(dim x, y as byte) as word
    result = x + y
end sub

main:
    trisB = 0
    while true
        ricavo = somma(10, 35)
        pere = 25
        mele = 30
        if somma(pere, mele) > 50 then
            portB.0 = 1
        end if
    wend
End.

Esempio di sub-function.

Definisco le variabili globali pere e mele di tipo byte e ricavo di tipo word.

Definisco la funzione somma con 2 parametri formali (x, y) di tipo byte e il risultato di tipo word.

Result è una variabile locale della funzione, creata automaticamente da mikroBasic, perciò non serve definirla. E' la variabile che riceverà il risultato della funzione (result = x + y). In questo caso, il compito che deve fare la funzione, è quello di fare la somma dei 2 valori passati nei parametri.

Nella main assegno a ricavo il risultato della funzione somma, chiamata passandole i parametri attuali (10, 35). Quando le istruzioni, eseguite dentro la funzione, saranno terminate, la variabile ricavo avrà il valore di result.

Poi assegno dei valori alle variabili pere e mele e, se la somma di queste, è maggiore di 50 (risultato della funzione somma), imposto ad 1 il bit portB.0.

Cos'è lo stack?

Quando un sottoprogramma (subroutine, procedura o funzione) viene chiamato, l'indirizzo dell'istruzione di ritorno deve essere salvato da qualche parte.
Lo stack è uno spazio di memoria indipendente al di fuori della RAM o ROM, dove vengono scritti gli indirizzi di ritorno dei sottoprogrammi.

La parola stack in inglese significa "catasta, pila" e su questa catasta e' possibile depositare, uno sull'altro, piu' indirizzi e poi recuperarli quando servono.

Questo tipo di memorizzazione viene anche denominata LIFO dall'inglese Last In First Out, in cui l'ultimo elemento inserito (last in) deve necessariamente essere il primo ad uscire (first out).

Grazie a questa caratteristica, è possibile effettuare più chiamate a sottoprogrammi annidate e mantenere sempre traccia del punto in cui proseguire l'esecuzione, nel momento in cui si incontra la fine del sottoprogramma.
program prova_nested
.....
.....

sub procedure ritardo
    delay_ms(10)
end sub

sub procedure proc_due
    ritardo
end sub

sub procedure proc_uno
    proc_due
end sub

main:
    .....
    .....
    proc_uno
    .....
    .....
    goto main
End.

Limitazioni di chiamate annidate

La chiamata annidata rappresenta, ad esempio, una chiamata, all'interno del corpo procedura, ad un'altra procedura o funzione.

Nell'esempio vediamo nella main

1) la chiamata a proc_uno, la quale, a sua volta,
2) chiama la proc_due, che a sua volta
3) chiama ritardo, che a sua volta
4) chiama delay_ms.

Tutte queste chiamate si dice che sono annidate ed i loro indirizzi di ritorno saranno impilati nello stack per essere presi al ritorno di ognuna di esse.

In questo caso, le richieste sono 3, perchè delay_ms, essendo inline, non ha effetto sullo stack.

mikroBasic limita il numero di chiamate annidate a:

  - 8 richieste per la famiglia PIC12,
- 8 richieste per la famiglia PIC16,
- 31 richieste per la famiglia PIC18.
 

Notare che le routine incorporate (built-in routines) non contano contro questo limite, a causa della loro realizzazione "inline".
Il numero di chiamate annidate permesse diminuisce di uno, se si usa uno degli operatori " * , / , % " nel codice.
Diminuisce ulteriormente di uno, se nel programma si usa l'interrupt.
Se si supera il numero permesso di chiamate annidate, mikroBasic segnala l'errore di stack overflow.

Passare parametri per valore e per riferimento

Program prova_byval
dim limone as byte

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

sub procedure gelato(dim banana as byte)
    limone = banana + 3
    banana = limone
end sub

main:
    trisB = 0
    while true
        limone = 2
        portB = 5
        pausa_ms(2000)
        gelato(portB)
        pausa_ms(2000)
    wend
end. 

Per valore (byval).

Quando dobbiamo passare delle variabili ai parametri delle procedure e funzioni, queste, per impostazione predefinita, vengono passate "per valore" e la parola chiave byval non è necessaria.

Quando, in un parametro di una procedura, si trova scritto ad es: gelato(dim banana as byte) è come se fosse scritto: gelato(dim byval banana as byte) perché, per default, è sott'inteso il passaggio per valore.

Ecco, a sinistra, un esempio che non fa niente, ma è solo per spiegare la differenza del passaggio per valore e per riferimento.

N.B.
Ho inserito un ritardo di 2 secondi per poter vedere il risultato sui LED connessi a portB.

In questo caso, viene chiamata la procedura gelato e viene passato, come parametro, la variabile portB (portB è un registro, una variabile di tipo byte è un registro, quindi... portB è una variabile di tipo byte).
Nella procedura gelato, il valore di banana, sarà la copia di quello di portB (cioè 5), che poi, nella procedura verrà sommato con 3 ed il risultato verrà messo in limone.
Poi banana assumerà il valore di limone.
Quando si ritorna dalla procedura, il valore di banana "muore" con la procedura perché, essendo un parametro formale, è riconosciuto come se fosse una variabile locale. Il valore di portB sarà ancora 5 ed il valore di limone sarà 8 (5+3), perché è una variabile globale ed è stata modificata nella procedura gelato.

Program prova_byref
dim limone as byte

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

sub procedure gelato(dim byref banana as byte)
    limone = banana + 3
    banana = limone
end sub

main:
    trisB = 0
    while true
        limone = 2
        portB = 5
        pausa_ms(2000)
        gelato(portB)
        pausa_ms(2000)
    wend
end.

Per riferimento (byref).

Se, invece, dobbiamo passare la variabile per riferimento dobbiamo usare la parola chiave byref.

Byref indica che la variabile viene presa per riferimento e cioè, si fa riferimento al suo indirizzo in memoria.

Vediamo con un'altro esempio, simile a quello di prima, le differenze.

N.B.
Anche qui, ho inserito un ritardo di 2 secondi per poter vedere il risultato sui LED connessi a portB.

Anche in questo caso, viene chiamata la procedura gelato e viene passato, come parametro, il registro portB.
Nella procedura gelato, il valore di banana sarà quello di portB (cioè 5), che poi, nella procedura, verrà sommato con 3 ed il risultato verrà messo in limone.
Poi banana assumerà il valore di limone.
Quando si ritorna dalla procedura, il valore di portB, però, non sarà ancora 5, ma avrà il valore di limone (8 cioè 5+3).
Perché succede questo? Non c'è nessuna istruzione che modifica portB. E allora, cosa è successo?
La differenza sta nel passaggio della variabile, per riferimento.
Banana, per riferimento, ha preso l'indirizzo del registro portB ed è come se fosse diventata portB, a tutti gli effetti. Ha solo cambiato nome MOMENTANEAMENTE. Se si modifica banana, si modifica, in realtà, portB.
Quando, nella procedura, si trova l'istruzione "banana = limone", è come se fosse stato scritto portB = limone.

Con byval, viene COPIATO il valore della variabile, mentre con byref, viene MODIFICATA la variabile direttamente.

Vediamo un esempio pratico analizzando ByteToStr, una procedura della Conversions Library.

   

La procedura ByteToStr converte un valore, di tipo byte, in una stringa di 3 caratteri per permettere la visualizzazione su display LCD.
Essa è definita con 2 parametri formali:
number, di tipo byte, che riceverà il numero da convertire;
output, di tipo string di 3 caratteri, che conterrà il risultato della conversione.
Notiamo che il primo parametro sarà passato per valore, mentre il secondo sarà passato per riferimento.

Essendo una procedura della libreria, non possiamo vedere il codice sorgente contenuto, però ci basta sapere quali dati dobbiamo fornire, per farla funzionare perfettamente.

Proviamo questo programmino, dove ho inserito anche gli argomenti che abbiamo trattato precedentemente.

program prova_param

dim testo_str as string[3]
dim num_byte as byte

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

sub function somma(dim x, y as byte) as byte
    result = x + y
end sub

Main:
    Lcd_Init(portB)
    Lcd_Cmd(Lcd_Cursor_Off)
    while true
        Lcd_Cmd(Lcd_Clear)
        num_byte = somma(105, 41)
        ByteToStr(num_byte, testo_str)
        Lcd_Out(1, 1, "Num_Byte e' :")
        Lcd_Out(2, 1, testo_str)
        pausa_ms(2000)
        Lcd_Cmd(Lcd_Clear)
        ByteToStr(somma(155, 34), testo_str)
        Lcd_Out(1, 1, "Somma e' :")
        Lcd_Out(2, 1, testo_str)
        pausa_ms(2000)
    wend

End.

Iniziamo definendo la variabile testo_str, di tipo string di 3 caratteri e la variabile num_byte, di tipo byte.
Poi definiamo la procedura pausa_ms (che abbiamo già visto) e la funzione somma (anch'essa già vista prima).

Nella main, inizializziamo il display LCD (Lcd_init) ed escludiamo la visualizzazione del cursore (Lcd_cursor_off), dopodichè entriamo in un ciclo infinito (while-wend).
Azzeriamo il display (Lcd_clear) ed assegnamo alla variabile num_byte, il risultato della funzione somma.

Ora vogliamo visualizzare il valore di num_byte e, per fare questo, dobbiamo convertire il valore numerico in una stringa di caratteri per trasferirla, poi, al display LCD il quale accetta solo caratteri.
Questo compito lo esegue la procedura ByteToStr e quindi passiamo, al primo parametro, il valore da convertire, num_byte.
Al secondo parametro passiamo, invece, il nome della variabile stringa che dovrà ricevere il risultato della conversione, testo_str; non passiamo il valore ma il riferimento alla variabile (l'indirizzo in memoria).

Adesso visualizziamo sul display (Lcd_out), nella prima riga, un messaggio e, nella seconda riga, la stringa testo_str indicante il valore di num_byte.
Attendiamo 2 secondi e riazzeriamo il display.

Adesso vediamo un'altro modo di passare parametri. Nella procedura ByteToStr, invece di passare il valore di una variabile di tipo byte nel primo parametro, possiamo passare il risultato (di tipo byte) di una funzione, in questo caso somma.
Il resto è uguale a quello che abbiamo visto prima e ripetiamo in un ciclo infinito.

sub procedure proc_1
    dim errore as byte
    ... 'qui facciamo qualcosa
    if errore = true then	
        exit
    end if
    ... 'codice che non verrà 
        'eseguito se errore è vero
end sub

Saltare fuori da una procedura o funzione

Se fosse necessario uscire da una procedura o funzione, senza eseguire parte del codice, esiste una istruzione apposita, exit.

Exit esegue un salto alla fine della procedura o funzione e torna al punto seguente la chiamata.

Uscendo da una funzione, il valore di ritorno sarà quello della variabile locale result, al momento dell'uscita.

Schemi elettrici

   
   

Bibliografia:
Manuale mikroBasic

Ultima modifica  

 
Privacy Policy Cookie Policy