7. Η εντολή CALL και ο σωρός

Η εντολή CALL και ο σωρός

Μια άλλη εντολή διακλάδωσης είναι η CALL, η οποία χρησιμοποιείται για να καλέσουμε μια υπορουτίνα. Οι υπορουτίνες χρησιμοποιούνται συχνά για να καλέσουμε κομμάτια κώδικα που εκτελούνται συχνά. Αυτό κάνει το πρόγραμμα περισσότερο δομημένο και έχουμε εξοικονόμηση  μνήμης προγράμματος. Στους μικροελεγκτές AVR έχουμε τέσσερις εντολές για να καλούμε υπορουτίνες: CALL (long Call), RCALL (relative Call), ICALL (indirect call to Z) και EICALL (extended indirect call to Z). Η επιλογή της κατάλληλης εντολής εξαρτάται από την διεύθυνση προορισμού. Η κάθε μια εντολή επεξηγείται αναλυτικά παρακάτω:

CALL

H CALL είναι μια 4-byte (32 bit) εντολή. Μπορεί να χρησιμοποιηθεί για να καλέσει υπορουτίνες που μπορούν να βρίσκονται οπουδήποτε μέσα στην μνήμη προγράμματος, της 4Μ περιοχής διευθύνσεων.

Για να γνωιζει ο μικροελεγκτής σε ποια εντολή θα συνεχίσει την εκτέλεση του προγράμματος μετά που θα τελειώσει την εκτέλεση της υπορουτίνας, ο AVR προτού αρχίσει να εκτελεί την υπορουτίνα αποθηκεύει αυτόματα στο σωρό την διεύθυνση της εντολής που βρίσκεται ακριβώς μετά την εντολή CALL.

Όταν μια υπορουτίνα καλείται, η εκτέλεση μεταφέρεται στην διεύθυνση της υπορουτίνας. Πρωτού η CPU μεταβεί στην διεύθυνση της υπορουτίνας αποθηκεύει τον PC (program counter) της επόμενης εντολής στο σωρό. Μετά την εκτέλεση της υπορουτίνας η εκτέλεση πάει πίσω στην εντολή που κάλεσε την υπορουτίνα και αρχίζει την εκτέλεση των επόμενων εντολών ακριβώς κάτω από την εντολή CALL.

Μετά την εκτέλεση όλων των εντολών της υπορουτίνας η εντολή RET μεταφέρει την εκτέλεση πίσω στο σημείο που κάλεσε την υπορουτίνα. Σε κάθε υπορουτίνα πρέπει να περιέχει την εντολή RET σαν τελευταία εντολή.

Ο σωρός και ο δείκτης σωρού στους AVR

Ο σωρός είναι ένα κομμάτι της μνήμης RAM που χρησιμοποιείται από την CPU για αποθήκευση δεδομένων προσωρινά. Αυτή η πληροφορία μπορεί να είναι δεδομένα ή διευθύνσεις. Η CPU χρειάζεται το σωρό διότι ο αριθμός των καταχωρητών είναι περιορισμένος.

Πως ο AVR έχει πρόσβαση στο σωρό

Στους μικροελεγκτές AVR υπάρχει μέσα στη CPU ένας καταχωρητής που δείχνει στο σωρό. Ο καταχωρητής που χρησιμοποιείται για να έχουμε πρόσβαση στο σωρό ονομάζεται SP (stack pointer).

Στην Ι/Ο περιοχή μνήμης υπάρχουν δυο καταχωρητές οι SPL (the low byte of the SP) και SPH (the high byte of the SP). Ο καταχωρητής δείκτης σωρού αποτελείται από δυο καταχωρητές. Ο SPH αναπαριστά το υψηλότερο byte του SP και ο SPL αναπαριστά το χαμηλότερο byte του δείκτη.

Το μέγεθος του δείκτη σωρού θα πρέπει να έχει μέγεθος τέτοιο ώστε που να μπορεί να χρησιμοποιηθεί για όλο το μέγεθος της RAM. Έτσι για τους AVR με μνήμη RAM μεγαλύτερη από 256 bytes o SP αποτελείται από δυο 8-bit καταχωρητές (SPL και SPH) ενώ για τους AVR με λιγότερη από 256 bytes μνήμη ο SP αποτελείται μόνο από τον SPL.

Η αποθήκευση πληροφορίας όπως τον program counter (PC) στο σωρό ονομάζεται PUSH ενώ η ανάκτηση πληροφορίας πίσω στη CPU ονομάζεται ΡΟΡ. Με άλλα λόγια ένας καταχωρητής pushed στο σωρό για αποθήκευση του και popped off για ανάκτηση του από το σωρό.

Σπρώχνοντας δεδομένα στο σωρό

Ο δείκτης του σωρού δείχνει στην κορυφή του σωρού. Καθώς βάζουμε δεδομένα στο σωρό, τα δεδομένα αποθηκεύονται εκεί που δείχνει ο δείκτης σωρού και στην συνέχεια ο δείκτης SP μειώνεται κατά ένα.

Για να σπρώξουμε δεδομένα στο σωρό χρησιμοποιούμε την εντολή PUSH

PUSH  Rr  ;  Rr μπορεί να είναι ένας γενικής χρήσης καταχωρητής (R0 – R31)

Για παράδειγμα για να αποθηκεύσουμε την τιμή του R10 έχουμε την ακόλουθη εντολή:

PUSH  R10  ;  αποθήκευσε τον R10 στο σωρό και μείωσε τον δείκτη SP

Ανακτώντας δεδομένα από το σωρό

Η ανάκτηση δεδομένων πίσω σε ένα καταχωρητή είναι η αντίστροφη διαδικασία της εισαγωγής δεδομένων στο σωρό. Καθώς η εντολή ΡΟΡ εκτελείται ο δείκτης σωρού αυξάνει κατά ένα και τα περιεχόμενα της κορυφής του σωρού αντιγράφονται πίσω στον καταχωρητή. Αυτό σημαίνει ότι ο σωρός είναι LIFO (Last – In – First – Out) μνήμη. Για να ανακτήσουμε ένα byte από το σωρό χρησιμοποιούμε την εντολή ΡΟΡ.

POP  Rr ; Rr μπορεί να είναι ένας από τους γενικού σκοπού καταχωρητές.

Για παράδειγμα η επόμενη εντολή ανακτά από την κορυφή του σωρού ένα byte και το αντιγράφει στον καταχωρητή R10.

POP  R16 ; αυξάνει τον δείκτη σωρού SP και μετά φορτώνει την κορυφή του σωρού στον R10

Παράδειγμα

.ORG 0
; αρχικοποίησε τον δείκτη ώστε να δείχνει στην τελευταία θέση της RAM
LDI R16, HIGH(RAMEND)
OUT SPH, R16
LDI R16, LOW(RAMEND)
OUT SPL, R16
LDI R18, 0
LDI R19, 0x3A
LDI R20, 0x5E
PUSH R19
PUSH R20
LDI R19, 0
LDI R20, 0
POP R20
POP R19

Αρχικοποιώντας τον δείκτη σωρού

Όταν ο AVR τροφοδοτείται με τάση ο δείκτης SP περιέχει την τιμή μηδέν η οποία είναι η διεύθυνση του καταχωρητή R0. Επομένως θα πρέπει να αρχικοποιούμε τον SP στην αρχή του προγράμματος μας ώστε να δείχνει μέσα στην εσωτερική SRAM. Στους AVRs ο σωρός αναπτύσσεται από την υψηλότερη θέση μνήμης προς την χαμηλότερη θέση μνήμης. Όταν σπρώχνουμε δεδομένα στο σωρό ο δείκτης SP μειώνεται. Έτσι συνίσταται να αρχικοποιούμε τον δείκτη SP στην υψηλότερη θέση μνήμης SRAM.

Διαφορετικοί τύποι μικροελεγκτών AVR έχουν διαφορετική ποσότητα μνήμης RAM. Στον AVR assembler η σταθερά RAMEND αναπαριστά την διεύθυνση της τελευταίας θέσης RAM. Έτσι εάν θέλουμε να αρχικοποιήσουμε τον SP έτσι ώστε να δείχνει στην τελευταία θέση μνήμης, μπορούμε απλά να φορτώσουμε την σταθερά RAMEND στον δείκτη SP. Σημείωση ο SP αποτελείται από δυο καταχωρητές τον SPH και SPL. Έτσι φορτώνουμε το υψηλότερο byte της RAMEND στον SPH και το χαμηλότερο byte της RAMEND στον SPL.

Η εντολή CALL και ο ρόλος του σωρού

Όταν μια υπορουτίνα καλείται η CPU πρώτα αποθηκεύει την διεύθυνση της εντολής που βρίσκεται μόλις κάτω από την εντολή CALL στο σωρό και μετά μεταφέρει την εκτέλεση σε αυτή την υπορουτίνα. Έτσι με αυτό τον τρόπο η CPU γνωρίζει που να συνεχίσει την εκτέλεση όταν επιστρέφει από την καλούμενη υπορουτίνα.

Για τους AVRs που ο program counter δεν είναι μεγαλύτερος από 16bits (π.χ. ATmega328) η περιεχόμενη τιμή τουprogram counter διαχωρίζεται σε 2bytes. Το υψηλότερο byte σπρώχνεται πρώτο στο σωρό και μετά το χαμηλότερο byte σπρώχνεται στο σωρό. Για τους AVRs που ο program counter έχει μέγεθος μεγαλύτερο από 16bits αλλά μικρότερο από 24bits, η τιμή του program counter διαχωρίζεται σε 3 bytes. Το υψηλότερο byte σπρώχνεται πρώτο στο σωρό, στη συνέχεια το μεσαίο byte και στο τέλος το χαμηλότερο byte σπρώχνεται στο σωρό.

Η εντολή RET και ο ρόλος του σωρού

Όταν η εντολή RET στο τέλος μιας υπορουτίνας εκτελείται τα δεδομένα της κορυφής του σωρού αντιγράφονται πίσω στον program counter και ο δείκτης του σωρού αυξάνει. Όταν η εντολή CALL εκτελείται η διεύθυνση της εντολής ακριβώς κάτω από την CALL τοποθετείται στο σωρό, έτσι ώστε όταν η εκτέλεση της υπορουτίνας τελειώσει και η εντολή RET εκτελεστεί, η διεύθυνση της εντολής κάτω από την CALL φορτώνεται στον PC και η εντολή ακριβώς κάτω από την εντολή CAL εκτελείται.

Το μέγιστο όριο του σωρού

Όπως έχουμε αναφέρει σε προηγούμενη ενότητα, μπορούμε να ορίσουμε το σωρό οπουδήποτε μέσα στην γενικού σκοπού μνήμη RAM. Έτσι στους AVR ο σωρός μπορεί να είναι μεγάλος όσος η RAM. Όμως δεν πρέπει να ορίσουμε το σωρό μέσα στη μνήμη που αντιστοιχεί στου γενικού σκοπού καταχωρητές ή στην Ι/Ο μνήμη. Δηλαδή ο δείκτης σωρού πρέπει να δείχνει υψηλότερα από την τιμή 0x60

Παράδειγμα

Γράψε ένα πρόγραμμα που να εναλλάζει όλα τα bits της θύρας Β στέλνοντας τις τιμές 0x55 και 0xAA συνεχόμενα. Εισήγαγε μια χρονοκαθυστέρηση σε κάθε εναλλαγή.

.ORG 0
         LDI R16, HIGH(RAMEND)
         OUT SPH, R16
         LDI R16, LOW(RAMEND)
BACK:    LDI R16, 0x55           ; φόρτωσε τον R16 με την τιμή 0x55
         OUT PORTB, R16          ; στείλε την τιμή 0x55 στην θύρα Β
         CALL DELAY              ; κάλεσμα υπορουτίνας χρονοκαθυστέρισης
         LDI R16, 0xAA           ; φόρτωσε τον R16 με την τιμή 0xAA
         OUT PORTB, R16          ; στείλε την τιμή 0xΑΑ στη θύρα Β
         CALL DELAY              ; κάλεσμα υπορουτίνας χρονοκαθυστέρισης
         RJUMP BACK              ; συνεχείς επανάληψη

;  υπορουτίνα χρονοκαθυστέρισης
.ORG 0x300
DELAY:
         LDI R21, 0xFF           ; φόρτωσε τον R21 με την τιμή 255
AGAIN:  
         NOP                     ; δεν κάνει τίποτα παρά καθυστέρηση      
         NOP
         DEC R21                 ; μείωση του R21 κατά ένα
         BRNE AGAIN              ; επανάλαβε μέχρι ο R21 γίνει μηδεν
         RET                     ; επιστροφή 

Στους AVR ο σωρός χρησιμοποιείται για την εκτέλεση υπορουτινών και εξυπηρέτηση διακοπών. Πρέπει να θυμόμαστε ότι όταν καλούμε υπορουτίνες ο σωρός ορίζει που η CPU θα επιστρέψει όταν τελειώσει την εκτέλεση μιας υπορουτίνας. Για αυτό το λόγο θα πρέπει να είμαστε προσεκτικοί όταν χειριζόμαστε τα περιεχόμενα του σωρού.

Καλώντας πολλές υπορουτίνες από το κυρίως πρόγραμμα

Στη γλώσσα Assembly είναι σύνηθες να έχουμε ένα κυρίως πρόγραμμα και πολλές υπορουτίνες που καλούνται από αυτό. Αυτό επιτρέπει να έχουμε κάθε υποτουτίνα σε ξεχωριστή ενότητα. Κάθε ενότητα μπορεί να αναπτυχθεί και να ελεγχθεί ξεχωριστά και μετά να ενωθεί με το κυρίως πρόγραμμα. Αυτό εξυπηρετεί στα μεγάλα προγράμματα όπου κάθε υπορουτίνα μπορεί να αναπτυχθεί από διαφορετικό προγραμματιστή.

Παράδειγμα

Γράψε ένα πρόγραμμα που να μετρά από 0 έως 0xFF και να εμφανίζει την ένδειξη στην θύρα Β. Χρησιμοποίησε μια CALL υπορουτίνα για να στέλνει την ένδειξη στην θύρα Β και μια για χρονοκαθυστέρηση.

             .DEF COUNT=R20
             .ORG 0
              LDI R16, HIGH(RAMEND)
              OUT SPH, R16
              LDI R16, LOW(RAMEND)
              OUT SPL, R16
              LDI COUNT, 0
BACK:         CALL DISPLAY
              RJMP BACK

DISPLAY:      INC COUNT
              OUT PORTB, COUNT
              CALL DELAY
              RET
               
              .ORG 0x300
DELAY:        LDI R16, 0xFF
AGAIN:        NOP
              NOP
              NOP
              DEC R16
              BRNE AGAIN
              RET

RCALL (relative call)

H RCALL είναι μια 2-byte εντολή σε αντίθεση με την CALL η οποία είναι μια 4-byte εντολή. Στην εντολή RCALL η διεύθυνση ετικέτας της υπορουτίνας πρέπει να είναι να είναι μέσα στα -2048 εως +2047 words μνήμης προγράμματος σχετικά με την τρέχων διεύθυνση που δείχνει ο program counter (PC).

Δεν υπάρχει διαφορά μεταξύ των εντολών RCALL και CALL ως προς τον τρόπο αποθήκευσης του program counter στο σωρό και την λειτουργία της εντολής RET. Η μόνη διαφορά είναι ο ορισμός της διεύθυνσης της ετικέτας της υπορουτίνας, η οποία για την CALL μπορεί να είναι οπουδήποτε μέσα στα 4Μ της μνήμης προγράμματος ενώ για την RCALL η διεύθυνση της υπορουτίνας πρέπει να είναι μέσα στα 4Κ σχετικά με τον program counter. Η χρήση της εντολής RCALL έναντι της CALL μπορεί να μας εξοικονομήσει χώρο στη μνήμη προγράμματος.

Παράδειγμα

Γράψε το προτελευταίο πρόγραμμα όσο πιο αποδοτικά μπορείς

.ORG 0
              LDI R16, HIGH(RAMEND)
              OUT SPH, R16
              LDI R16, LOW(RAMEND)
              OUT SPL, R16
              LDI R16, 0x55
BACK:         COM R16
              OUT PORTB, R16
              RCALL DELAY
              RJMP BACK

DELAY:        LDI R20, 0xFF
AGAIN:        NOP
              NOP
              DEC R20
              BRNE AGAIN
              RET

ICALL (indirect call)

Σε αυτή την 2-byte (16bit) εντολή, ο καταχωρητής Ζ ορίζει την διεύθυνση της υπορουτίνας. Όταν η εντολή εκτελείται, η διεύθυνση της επόμενης εντολής αποθηκεύεται στο σωρό και ο program counter φορτώνεται με τα περιεχόμενα του καταχωρητή Ζ. Έτσι ο καταχωρητής Ζ θα πρέπει να περιέχει την διεύθυνση της υπορουτίνας όταν η εντολή ICALL εκτελείται.

Επειδή ο καταχωρητής Ζ έχει μέγεθος 16bit, η εντολή ICALL μπορεί να καλεί υπορουτίνες μέσα στις χαμηλότερες 64Κ words της μνήμης προγράμματος. Για τους AVRs που έχουν περισσότερη από 64Κ words μνήμης προγράμματος η εντολή EICALL (extended indirect call) είναι διαθέσιμη.

Η εντολή EICALL φορτώνει τον καταχωρητή Ζ στα χαμηλότερα 16bits του program counter (PC) και τα περιεχόμενα του καταχωρητή EIND στα ανώτερα 6 bits του PC. Σημείωση: ο EIND είναι μέρος της Ι/Ο μνήμης.