Το προφίλ μας στο Google Plus
0

Κατασκευάστε έναν υπολογιστή, μέρος 4: GPU με απ’ όλα

Το κύκλωμα γραφικών δεν ασχολείται μόνο με την παραγωγή του σήματος VGA. Η GPU έχει αποκλειστική πρόσβαση στη frame buffer κι αυτό την επιφορτίζει με αρκετά καθήκοντα. Οποιαδήποτε επέμβαση στην εικόνα, από τον καθαρισμό έως την εμφάνιση ενός χαρακτήρα ή μιας φωτογραφίας, αποτελεί ευθύνη του προγράμματός μας.

Στους πρώτους “προσωπικούς υπολογιστές” τα κυκλώματα γραφικών δεν ενσωμάτωναν κάποιο τσιπ για τη συμπίεση και την αποσυμπίεση βίντεο, ούτε Pixel Shader, ούτε Stream Processor, ούτε τα τεράστια ποσά μνήμης που συναντάμε στις σύγχρονες κάρτες γραφικών. Μη νομίζετε όμως ότι είχαν περιορισμένες αρμοδιότητες. Εκείνα τα κυκλώματα, όπως και το δικό μας, περιλάμβαναν ένα μικρό ποσό μνήμης για την αποθήκευση της εικόνας, επάνω στο οποίο δεν μπορούσε να γράψει κανένας άλλος. Ως εκ τούτου, οτιδήποτε εμφανιζόταν στην οθόνη είχε τοποθετηθεί στη frame buffer από το ίδιο το κύκλωμα γραφικών. Αυτό δεν είναι μικρό πράγμα! Οι GPU της εποχής είχαν την ευθύνη της σχεδίασης των χαρακτήρων και από αυτή την άποψη ενσωμάτωναν τη μοναδική γραμματοσειρά του συστήματος. Στην καλύτερη περίπτωση μπορεί να διέθεταν δύο ή τρεις γραμματοσειρές, στις οποίες άλλαζε μόνο το μέγεθος των γραμμάτων και η καθεμία προοριζόταν για μία από τις υποστηριζόμενες αναλύσεις. Αν αυτό δεν σας κάνει εντύπωση, σκεφτείτε ότι εκείνες οι GPU αναλάμβαναν και τη σχεδίαση του δρομέα (cursor), ενώ στη λίστα με τα προσόντα τους συναντούσε κανείς τη σχεδίαση απλών γεωμετρικών σχημάτων, όπως ευθύγραμμα τμήματα, τετράγωνα και κύκλους. Μάλιστα οι πιο προχωρημένες προσέφεραν και την (εκπληκτικής ισχύος) δυνατότητα του color flood. Δηλαδή μπορούσαν να γεμίσουν μια κλειστή περιοχή της εικόνας με ένα συγκεκριμένο χρώμα.

Πιστεύουμε ότι καταλαβαίνετε πού το πάμε: Παρόμοια προσόντα με τις παλιές GPU, διαθέτει και η δική μας. Τα καθήκοντά της δεν περιορίζονται στην παραγωγή των scanlines και κατ’ επέκταση του σήματος VGA. Διαθέτει ένα πρωτόλειο API, μέσω του οποίου ο επεξεργαστής του υπολογιστή μπορεί να “παραγγείλει” τον καθαρισμό της οθόνης, την “εκτύπωση” ενός χαρακτήρα, την εμφάνιση ενός pixel, τη μετατόπιση του δρομέα και πάει λέγοντας.

Κώδικας επικοινωνίας
Όπως είδαμε στο άρθρο που αρχίζει από τη σελίδα PQRXYZ, η κρισιμότερη εργασία του προγράμματος επιτελείται μέσα στη ρουτίνα scanline. Η συγκεκριμένη ρουτίνα εκτελείται σε τακτά χρονικά διαστήματα με τη βοήθεια ενός interrupt, το οποίο παράγεται από τον timer/counter1. Όλο τον υπόλοιπο χρόνο το πρόγραμμα βρίσκεται μέσα σ’ έναν βρόχο και περιμένει εντολές από τον επεξεργαστή. Λαμβάνοντας υπόψη το μικρό πλήθος λειτουργιών που προσφέρει η GPU καθώς και τη χαμηλή ταχύτητα επικοινωνίας με τον επεξεργαστή, ορίσαμε ένα απλούστατο σύστημα επικοινωνίας. Κάθε εισερχόμενο byte με τιμή μικρότερη του 128 αντιμετωπίζεται ως κωδικός ASCII και το πρόγραμμα φροντίζει να εμφανίσει στην οθόνη τον αντίστοιχο χαρακτήρα. Βέβαια, στη συγκεκριμένη περιοχή τιμών συγκαταλέγονται και οι χαρακτήρες ελέγχου (LINE FEED, CARRIAGE RETURN, BACKSPACE κ.ά.), οι οποίοι δεν είναι εκτυπώσιμοι. Το πρόγραμμα προβλέπει τη λήψη τέτοιων χαρακτήρων και μπορεί να μετατοπίζει το δρομέα στην κατάλληλη θέση, να διαγράφει χαρακτήρες κ.λπ. Όσα bytes στέλνει ο επεξεργαστής κι έχουν τιμή μεγαλύτερη του 127 αντιμετωπίζονται ως εντολές. Κάποιες από αυτές, όπως ο καθαρισμός της οθόνης και η “επανεκκίνηση” του κυκλώματος γραφικών, δεν δέχονται παραμέτρους. Υπάρχουν όμως κι εντολές, όπως εκείνη για την αλλαγή χρώματος των χαρακτήρων, που συνοδεύονται πάντα από παραμέτρους. Όταν λαμβάνονται εντολές της δεύτερης κατηγορίας το πρόγραμμα αντιμετωπίζει τα επόμενα bytes ως παραμέτρους και δεν πραγματοποιεί το διαχωρισμό σε χαρακτήρες και εντολές. Μετά τη λήψη των παραμέτρων και αφού εκτελεσθεί η εντολή, η ροή του προγράμματος επιστρέφει στον κεντρικό βρόχο και περιμένει νέες εντολές και νέους χαρακτήρες.

Τα προσόντα μιας προϊστορικής GPU: Προσφέρει διάφορες αναλύσεις, αναλαμβάνει τη διαχείριση του δρομέα και ενσωματώνει μέχρι και  interface για ποντίκι και light pen. Τα τσιπάκια γραφικών εκείνης της εποχής ήταν πολυτάλαντα, αν και δεν ήταν ιδιαίτερα αποδοτικά στο χειρισμό διανυσματικών μεγεθών ;)

Τα προσόντα μιας προϊστορικής GPU: Προσφέρει διάφορες αναλύσεις, αναλαμβάνει τη διαχείριση του δρομέα και ενσωματώνει μέχρι και interface για ποντίκι και light pen. Τα τσιπάκια γραφικών εκείνης της εποχής ήταν πολυτάλαντα, αν και δεν ήταν ιδιαίτερα αποδοτικά στο χειρισμό διανυσματικών μεγεθών ;)

Πολλαπλές διακλαδώσεις
Κάθε φορά που ο επεξεργαστής στέλνει “απλούς” χαρακτήρες, εκτελείται η ρουτίνα εκτύπωσης. Οι εντολές, ωστόσο, δεν έχουν υλοποιηθεί σε μία ενιαία ρουτίνα. Η κοινή λογική επέβαλε το διαχωρισμό τους σε επιμέρους τμήματα κώδικα, ώστε το πρόγραμμα να είναι ευανάγνωστο και διαχειρίσιμο. Έτσι, κάθε φορά που καταφτάνει μια εντολή, το πρόγραμμα πρέπει να καλέσει την αντίστοιχη ρουτίνα. Τώρα μπορεί να αναρωτιέστε γιατί συζητάμε κάτι τόσο τετριμμένο. Θα μπορούσαμε να χρησιμοποιήσουμε μια δομή σαν τη switch και να ξεμπερδέψουμε αμέσως. Στην Assembly όμως δεν υπάρχει τίποτα παραπλήσιο. Μια εναλλακτική λύση θα ήταν να συνδυάσουμε μερικούς απλούς ελέγχους:

if (received_byte < 128)
    call print_character
else if (received_byte == 200)
    call command1
else if (received_byte == 201)
    call command2
else if (received_byte == 202)
    call command3
...

O παραπάνω ψευδοκώδικας είναι ευανάγνωστος, αλλά όχι κι αποδοτικός. Όσο αυξάνεται το πλήθος των εντολών και συνεπώς το πλήθος των ελέγχων, εισάγονται μεγαλύτερες καθυστερήσεις. Φανταστείτε τώρα να γράφαμε κάτι τέτοιο σε Assembly. Επειδή η εντολή “else if” δεν υπάρχει, στην περίπτωση που επαληθευόταν ένας από τους ελέγχους θα έπρεπε να φροντίσουμε εμείς για την παράκαμψη των υπολοίπων. Κάτι τέτοιο θα μπορούσε να γίνει τοποθετώντας “κάτω” από κάθε έλεγχο ένα μικρό μπλοκ κώδικα, με την κλήση της αντίστοιχης ρουτίνας και μία εντολή για την παράκαμψη των περιττών ελέγχων. Αυτό δεν ακούγεται και πολύ κακό, αλλά υπάρχει ένα μικρό πρόβλημα: Στην Assembly δεν υφίσταται η έννοια του μπλοκ κώδικα και το ζήτημα περιπλέκεται ακόμα περισσότερο. Όπως καταλαβαίνετε, υλοποιώντας την παραπάνω λύση σε Assembly το πρόγραμμα θα καταντούσε δυσανάγνωστο. Για αυτούς τους λόγους κι επειδή μιλάμε για ένα κομβικό τμήμα του προγράμματος, καταφύγαμε στη λύση των jumplists.

Ο δείκτης του δείκτη
Ας ξαναδούμε εν συντομία το πρόβλημα. Κάθε φορά που η GPU λαμβάνει μια εντολή, το πρόγραμμα πρέπει να καλέσει την αντίστοιχη ρουτίνα. Αν βρίσκαμε κάποιον τρόπο για να συσχετίσουμε τις ίδιες τις εντολές (τις τιμές των bytes) με τις διευθύνσεις των αντίστοιχων ρουτινών, το πρόβλημα θα εξαφανιζόταν. Το πρόγραμμα θα υπολόγιζε τη διεύθυνση της κατάλληλης ρουτίνας και θα την καλούσε, χωρίς να πραγματοποιηθεί μια ορδή ελέγχων. Τα jumplists εξυπηρετούν στην κατασκευή ακριβώς ενός τέτοιου μηχανισμού. Πρόκειται για απλούς πίνακες, κάθε στοιχείο των οποίων αποτελεί τη διεύθυνση εκκίνησης μιας συνάρτησης. Το jumplist για το δικό μας πρόγραμμα ορίστηκε ως εξής:

jumplist:
.dw reset   ; index 00
.dw clear   ; index 01
.dw pset    ; index 02
.dw line    ; index 03
.dw box     ; index 04
.dw locate  ; index 05
...

Το “.dw” αποτελεί οδηγία προς τον assembler και όχι εντολή του μικροελεγκτή. Το αποτέλεσμά της είναι η αποθήκευση ενός word (δύο bytes) στη μνήμη προγράμματος. Οι λέξεις reset, clear, pset κ.λπ. αποτελούν τα ονόματα των ρουτινών που έχουμε κατασκευάσει για την εκτέλεση κάθε εντολής. Αυτά τα ονόματα αντικαθίστανται αυτομάτως με τις διευθύνσεις μνήμης των ρουτινών. Επειδή ο μικροελεγκτής που χρησιμοποιούμε διαθέτει 64ΚΒ για την αποθήκευση του κώδικα, οι διευθύνσεις έχουν μήκος ακριβώς 16 bits (δύο bytes). Τελικά, οι παραπάνω γραμμές δημιουργούν έναν πίνακα με τις διευθύνσεις εκκίνησης των ρουτινών. Τώρα μένει να δούμε πώς τον αξιοποιήσαμε. Ούτε κι αυτό είναι δύσκολο, αλλά θέλει λίγη προσοχή παραπάνω.

Ο κυρίως βρόχος του προγράμματος. Η GPU ξοδεύει ένα μεγάλος μέρος του χρόνου της εδώ, περιμένοντας από τον κεντρικό επεξεργαστή εντολές και χαρακτήρες προς εκτύπωση. Για την εκτέλεση των εντολών αξιοποιείται ένα jumplist.

Ο κυρίως βρόχος του προγράμματος. Η GPU ξοδεύει ένα μεγάλος μέρος του χρόνου της εδώ, περιμένοντας από τον κεντρικό επεξεργαστή εντολές και χαρακτήρες προς εκτύπωση. Για την εκτέλεση των εντολών αξιοποιείται ένα jumplist.

Κατ’ αρχάς, επειδή η GPU υποστηρίζει μόλις δώδεκα εντολές αποφασίσαμε να τις αντιστοιχίσουμε στις τιμές από 200 έως και 211. Έτσι, όταν το πρόγραμμα λαμβάνει κάποιο byte που δεν αντιστοιχεί σε χαρακτήρα, αφαιρεί από την τιμή του το 200. Αν το αποτέλεσμα είναι αρνητικό, θεωρούμε ότι συνέβη κάποιο σφάλμα και το συγκεκριμένο byte αγνοείται (το πρόγραμμα επιστρέφει στην αρχή του κυρίως βρόχου):

subi data, 200            ; subtract 200
brlo main_loop

Αν το αποτέλεσμα της αφαίρεσης είναι θετικό, το θεωρούμε αυτόματα ως τον αύξοντα αριθμό της επιθυμητής εντολής. Το επόμενο βήμα είναι να διαβάσουμε το αντίστοιχο στοιχείο από τον πίνακα jumplist. Για το σκοπό αυτό αποθηκεύουμε τη διεύθυνση εκκίνησης του πίνακα στον καταχωρητή Ζ. Προσέξτε τώρα το εξής: Κάθε στοιχείο του εν λόγω πίνακα έχει μήκος δύο bytes. Συνεπώς, για να διαβάσουμε το πέμπτο στοιχείο πρέπει να προσθέσουμε στον καταχωρητή Ζ το 10, για να διαβάσουμε το τρίτο στοιχείο πρέπει να προσθέσουμε το 6 κ.ο.κ. Με άλλα λόγια, πρέπει να διπλασιάζουμε τον αύξοντα αριθμό του επιθυμητού στοιχείου και να προσθέτουμε το αποτέλεσμα στον Z. Αυτό ακριβώς επιτυγχάνουν οι ακόλουθες γραμμές:

ldi ZH, high(jumplist*2)  ; get jumplist starting address
ldi ZL, low(jumplist*2)   ;
lsl data                  ; double the offset (table contains words!)
add ZL, data              ; add offset to low byte jumplist pointer
adc ZH, zero              ;

Ο διπλασιασμός του αύξοντα αριθμού της εντολής πραγματοποιείται στην τρίτη γραμμή. Η εντολή lsl (logic shift left) ολισθαίνει τα bit ενός καταχωρητή προς τα αριστερά και στη θέση του πρώτου εισάγει ένα μηδενικό. Με αυτόν τον τρόπο η τιμή του καταχωρητή διπλασιάζεται με τον πιο γρήγορο τρόπο. Αν χρησιμοποιούσαμε την εντολή πολλαπλασιασμού, θα έπρεπε να αποθηκεύσουμε τον πολλαπλασιαστή (2) σε κάποιον πρόσθετο καταχωρητή κι αυτό θα συνιστούσε “πολλή φασαρία για το τίποτα”. Με όσα έχουμε δει ως τώρα το πρόγραμμα υπολογίζει τη διεύθυνση ενός στοιχείου του πίνακα jumplist, που αντιστοιχεί στην προς εκτέλεση εντολή. Αυτό το στοιχείο, με τη σειρά του, αποτελεί τη διεύθυνση της αντίστοιχης συνάρτησης και απαρτίζεται από δύο bytes. Ο κώδικας συνεχίζει με την ανάγνωση αυτών των bytes και την αποθήκευσή τους στον καταχωρητή Ζ:

lpm tmp1, Z+              ; store low-byte of address-to-call in tmp1
lpm ZH, Z	              ; get high-byte of address-to-call in ZH
mov ZL, tmp1              ; move tmp1 to ZL

Αυτό ήταν όλο! Ο Z περιλαμβάνει πλέον τη διεύθυνση της ρουτίνας που πρέπει να εκτελεστεί και αρκεί να πραγματοποιήσουμε την κλήση.

icall                     ; call routine at address Z

Ο μηχανισμός που στήσαμε γύρω από τον πίνακα jumplist επιτρέπει την εύκολη προσθαφαίρεση εντολών, χωρίς να αλλάζει η έκταση ή ο χρόνος εκτέλεσης του κώδικα. Αρκεί οι διευθύνσεις των ρουτινών που έχουμε τοποθετήσει στον πίνακα να βρίσκονται σε αντιστοιχία με τις αριθμητικές τιμές των εντολών. Για παράδειγμα, αν η δεύτερη εντολή πραγματοποιεί τον καθαρισμό την οθόνης, το δεύτερο στοιχείο του πίνακα jumplist πρέπει να είναι η διεύθυνση εκκίνησης της ρουτίνας που καθαρίζει την οθόνη. Δεν ξέρουμε τι πιστεύετε εσείς, αλλά το σημαντικότερο προσόν αυτού του μηχανισμού θεωρούμε ότι σχετίζεται με την κομψότητα ;)

Εσωτερική ισορροπία
Η ρουτίνα scanline “σπάει το κοντέρ” κατά τη διάρκεια του ορατού τμήματος της γραμμής. Σε αυτή τη φάση πετυχαίνει το μέγιστο pixel clock που επιτρέπουν τα χαρακτηριστικά του μικροελεγκτή και δεν της μένει χρόνος για τίποτε άλλο. Αυτό δεν σημαίνει ότι τα πράγματα είναι χαλαρά κατά τη διάρκεια του παλμού HSYNC ή των διαστημάτων back porch και front porch. Αν εξετάσετε τον κώδικα θα διαπιστώσετε ότι εντός αυτών των διαστημάτων υπάρχουν αρκετά nop (εντολές που δεν κάνουν τίποτα κι απλά καθυστερούν την εκτέλεση κατά έναν κύκλο). Αναρωτιέστε γιατί προστέθηκαν και γιατί δεν βάλαμε στη θέση τους κάτι άλλο, περισσότερο χρήσιμο; Σκεφτείτε ότι ο κώδικας περιλαμβάνει αρκετές διακλαδώσεις. Αυτό σημαίνει ότι η εκτέλεσή του δεν ακολουθεί πάντοτε την ίδια πορεία: Κάποιες φορές εκτελείται το ένα τμήμα μιας διακλάδωσης και κάποιες φορές το άλλο. Όπως έχουμε πει, όμως, η ρουτίνα scanline πρέπει να εκτελείται με απόλυτα σταθερή συχνότητα κι αυτός ο “νόμος” ισχύει και για το εσωτερικό της — για όλα τα επιμέρους τμήματα της γραμμής. Για να πετύχουμε την “εσωτερική” σταθερότητα πρέπει να εξασφαλίσουμε ότι όλες οι πιθανές πορείες εκτέλεσης έχουν την ίδια διάρκεια. (Σ.τ.Ε. Εγώ πάλι νόμιζα ότι η εσωτερική σταθερότητα διασφαλίζεται με την ανάπτυξη…) Με άλλα λόγια, πρέπει να εξετάσουμε όλες τις διακλαδώσεις και για καθεμία να μετρήσουμε το χρόνο εκτέλεσης κάθε σκέλους. Αν κάποιο από τα δύο είναι γρηγορότερο πρέπει να το καθυστερήσουμε τόσο, ώστε να διαρκεί όσο το άλλο. Εδώ ακριβώς βοηθάνε τα nop. Κι επειδή οι συγκεκριμένες εντολές δεν εκτελούνται πάντα, είναι πρακτικά αδύνατο να αντικατασταθούν από άλλες, που θα έκαναν κάτι χρήσιμο.

Χειρισμός χαρακτήρων
Έχουμε αναφερθεί αρκετές φορές στη ρουτίνα εκτύπωσης, αλλά μέχρι στιγμής δεν έχουμε πει τίποτα για τον κώδικά της. Μιλάμε για τη ρουτίνα putchar, που καλείται από τον κύριο βρόχο του προγράμματος κάθε φορά που καταφτάνει κάποιος χαρακτήρας. Μπορείτε να δείτε τον κώδικά της στο αρχείο 1.prep.asm. Όπως θα διαπιστώσετε, η συγκεκριμένη ρουτίνα πραγματοποιεί έναν βασικό έλεγχο για το αν ο εισερχόμενος χαρακτήρας είναι εκτυπώσιμος ή όχι. Στην πρώτη περίπτωση καλείται η ρουτίνα print_char, ενώ στη δεύτερη καλείται η ρουτίνα print_ctrl. Η πρώτη σχεδιάζει τον εκάστοτε χαρακτήρα στη frame buffer, ενώ η δεύτερη πραγματοποιεί τη λειτουργία που περιγράφει ο εκάστοτε χαρακτήρας ελέγχου. Αυτές οι δύο ρουτίνες επιδρούν στο περιεχόμενο της frame buffer και τις έχουμε τοποθετήσει στο αρχείο 2.buffer.asm.

Συνήθως, η print_ctlr περιορίζεται σε μια μετακίνηση του δρομέα και σπανίως τροποποιεί την εικόνα. Ακόμα κι όταν συμβαίνει κάτι τέτοιο, π.χ., για τη διαγραφή ενός χαρακτήρα, επιστρατεύεται η print_char για την εκτύπωση ενός κενού στην κατάλληλη θέση. Όπως αντιλαμβάνεστε, η ρουτίνα print_ctrl δεν παρουσιάζει ιδιαίτερο ενδιαφέρον. Επιπρόσθετα, καθώς οι κωδικοί ASCII των χαρακτήρων ελέγχου δεν έχουν διαδοχικές τιμές, η χρήση ενός jumplist θα περιέπλεκε τα πράγματα περισσότερο. Για αυτό το λόγο καταλήξαμε να γράψουμε ένα κατεβατό από διαδοχικούς ελέγχους. Αν εξετάσετε τον κώδικα, πιστεύουμε ότι θα συμφωνήσετε αμέσως με την εκτίμηση που εκφράσαμε νωρίτερα: οι “αλυσιδωτοί” έλεγχοι στην Assembly προκαλούν σύγχυση.

Τελικά, το περισσότερο ενδιαφέρον συγκεντρώνεται στη ρουτίνα print_char. Για τη GPU, κάθε χαρακτήρας αποτελεί μια μικροσκοπική εικόνα. Επομένως, κάθε φορά που λαμβάνεται ένας εκτυπώσιμος χαρακτήρας η ρουτίνα print_char αντιγράφει την αντίστοιχη εικόνα στη frame buffer. Οι εν λόγω εικόνες έχουν διαστάσεις 8×10 pixels (πλάτος x ύψος) και είναι μονόχρωμες. Αυτό σημαίνει ότι κάθε pixel αναπαρίσταται ακριβώς από ένα bit και, κατ’ επέκταση, κάθε γραμμή αναπαρίσταται από ένα byte. Έτσι, η όψη κάθε χαρακτήρα περιγράφεται από 10 bytes. Όλες αυτές οι εικόνες βρίσκονται αποθηκευμένες στον πίνακα fontdata ο οποίος, όπως αντιλαμβάνεστε, αποτελεί τη γραμματοσειρά του υπολογιστή μας. Μπορείτε να δείτε το περιεχόμενο του πίνακα στο αρχείο 3.font.asm.

Η σχεδίαση της γραμματοσειράς πραγματοποιήθηκε χαρακτήρα προς χαρακτήρα, σε ένα 'ανακυκλωμένο' πρόγραμμα.

Η σχεδίαση της γραμματοσειράς πραγματοποιήθηκε χαρακτήρα προς χαρακτήρα, σε ένα “ανακυκλωμένο” πρόγραμμα.

Αν μελετήσετε τον κώδικα που εμφανίζει τους χαρακτήρες στην οθόνη, θα διαπιστώσετε ότι περιλαμβάνει δυο εμφωλευμένους βρόχους. Ο εξωτερικός (ξεκινά με την ετικέτα get_font_byte) διατρέχει τα δέκα bytes που απαρτίζουν την όψη του προς εκτύπωση χαρακτήρα. Ο εσωτερικός βρόχος (ξεκινά με την ετικέτα parse_bits) εξετάζει ένα προς ένα τα bits του εκάστοτε byte. Όταν κάποιο bit έχει την τιμή 1, τοποθετείται στη frame buffer ένα pixel με το χρώμα που περιγράφει ο καταχωρητής color. Στην αντίθετη περίπτωση τοποθετείται ένα pixel με το χρώμα που περιγράφει ο καταχωρητής paper. Με αυτόν τον τρόπο, η όψη κάθε χαρακτήρα μεταφέρεται στην εικόνα γραμμή προς γραμμή. Οι καταχωρητές color και paper χρησιμοποιούνται σαν δύο μεταβλητές global και το περιεχόμενό τους αποτελεί το χρώμα των γραμμάτων και το χρώμα του υποβάθρου αντίστοιχα. Παρεμπιπτόντως, σημειώστε ότι η GPU περιλαμβάνει δύο εντολές που επιτρέπουν στον επεξεργαστή να τροποποιήσει αυτά τα χρώματα.

Πριν προχωρήσουμε πρέπει να απαντήσουμε σ’ ένα ερώτημα που ενδέχεται να σας προβληματίζει: Πώς σχεδιάσαμε την όψη των χαρακτήρων; Γι’ αυτή τη δουλειά αξιοποιήσαμε ένα πρόγραμμα που είχαμε κατασκευάσει στο μακρινό παρελθόν. Αναφερόμαστε σ’ ένα υποτυπώδες σχεδιαστικό εργαλείο, με το οποίο δημιουργούσαμε “γραφικά” για ένα μονόχρωμο dot matrix 8×8. Το πρόγραμμα ήταν γραμμένο σε flash και παρόλο που τώρα δεν συμπαθούμε καθόλου τη συγκεκριμένη πλατφόρμα, θα ήταν κρίμα να πετάξουμε τόση δουλειά. Στο κάτω κάτω, η σχεδίαση των χαρακτήρων θα γινόταν μόνο μία φορά. Έτσι, με ελάχιστες επεμβάσεις μετατρέψαμε το παλιό πρόγραμμα σε εργαλείο σχεδίασης χαρακτήρων. Αν θέλετε να παίξετε κι εσείς μαζί του μπορείτε να το κατεβάσετε από εδώ.

Ξεζούμισμα
Η ρουτίνα scanline εκτελείται με μεγάλη συχνότητα και διακόπτει όλες τις υπόλοιπες εργασίες, συνεχώς. Γι’ αυτό το λόγο, μία από τις πρώτες ενέργειες μέσα στη ρουτίνα είναι η αποθήκευση της κατάστασης των ακροδεκτών της θύρας PORTB. Όπως φαίνεται και στο κύκλωμα, οι συγκεκριμένοι ακροδέκτες χρησιμοποιούνται για την ανταλλαγή κάποιων σημάτων με τον κεντρικό επεξεργαστή, ρυθμίζουν τη λειτουργία της frame buffer, ενώ ελέγχουν και τη ροή των δεδομένων στο data bus. Εν ολίγοις, η κατάσταση αυτών των ακροδεκτών αποτυπώνει την γενική κατάσταση της GPU και συνδέεται στενά με όλες τις λειτουργίες του κυκλώματος. Αμέσως μετά αποθηκεύεται και η τιμή του δείκτη Z, που ενδέχεται να ήταν σε χρήση. Λίγο αργότερα, αν βρισκόμαστε σε ορατή γραμμή, που σημαίνει ότι θα χρειαστεί να διαβαστούν δεδομένα από τη frame buffer, αποθηκεύεται και η κατάσταση των ακροδεκτών των υπολοίπων θυρών. Φυσικά, λίγο πριν τερματιστεί η ρουτίνα scanline πραγματοποιείται η αντίστροφη διαδικασία: Οι τιμές που είχαν αποθηκευτεί νωρίτερα τοποθετούνται και πάλι στις αντίστοιχες θέσεις. Έτσι, με τον τερματισμό της ρουτίνας ο μικροελεγκτής μπορεί να συνεχίσει την προηγούμενη εργασία του κανονικά, σαν μην έγινε η διακοπή ποτέ. Σε ένα οποιοδήποτε άλλο πρόγραμμα, η προσωρινή αποθήκευση των τιμών θα γινόταν στη στοίβα. Στο πρόγραμμα που εξετάζουμε, όμως, ακολουθήσαμε μια διαφορετική οδό. Οι 8μπιτοι μικροελεγκτές AVR ενσωματώνουν στον πυρήνα τους 32 καταχωρητές, για την προσωρινή αποθήκευση δεδομένων. Μιλάμε για τον πιο πολύτιμο πόρο των μικροελεγκτών, αφού οι συγκεκριμένοι καταχωρητές μπορούν να λάβουν μέρος άμεσα, σε κάθε πιθανή πράξη. Ωστόσο, εμείς αποφασίσαμε εξαρχής ότι θα αξιοποιούσαμε μόνο τους 22. Με αυτή τη γενναία σχεδιαστική επιλογή θέλαμε να εξασφαλίσουμε ότι μέσα στη ρουτίνα scanline θα υπάρχουν πάντα δέκα “ελεύθεροι” καταχωρητές. Σε αυτούς θα αποθηκεύαμε προσωρινά τις διάφορες τιμές και θα αποφεύγαμε τη στοίβα εντελώς. Αναρωτιέστε γιατί μπήκαμε σε αυτό το μπελά; Οι εντολές push και pop (προσθήκη και απομάκρυνση ενός byte από τη στοίβα) διαρκούν από δύο κύκλους η καθεμία. Αντίθετα, οι εντολές mov και movw (μετακίνηση ενός byte κι ενός word μεταξύ καταχωρητών) εκτελούνται σε έναν κύκλο η καθεμία. Καθώς η ρουτίνα scanline αποθηκεύει και επαναφέρει δέκα bytes, με τη μέθοδο που ακολουθήσαμε καταφέραμε να κερδίσουμε 20 κύκλους! Κι όπως αντιλαμβάνεστε, δεν μπορούσαμε να χαραμίσουμε κανέναν.

Ταχύτατη κύλιση
Όταν ολοκληρώσαμε τον κώδικα για την εμφάνιση των χαρακτήρων, ήρθαμε αντιμέτωποι με ένα νέο ζήτημα. Τι θα γινόταν όταν γέμιζε η οθόνη με χαρακτήρες ή, τέλος πάντων, όταν θα τυπωνόταν ένας χαρακτήρας στην τελευταία στήλη της τελευταίας γραμμής; Το λογικό θα ήταν να “ρολάρει” η εικόνα προς τα πάνω: να μετατοπιστούν τα περιεχόμενά της κατά μία γραμμή κειμένου προς τα πάνω και στο κάτω άκρο να εμφανιστεί μια κενή γραμμή. Για το σκοπό αυτό γράψαμε τη ρουτίνα roll, που στην αρχική της μορφή ήταν αρκετά μεγάλη και βραδυφλεγής! Βλέπετε, η πρώτη προσέγγιση στο ζήτημα ήταν αφελής. Ο κώδικας αντέγραφε κάθε pixel της frame buffer λίγες γραμμές πιο πάνω. Οι αντιγραφές ξεκινούσαν από τη 11η γραμμή pixel, η οποία κατέληγε στη θέση της 1ης και τερματίζονταν στην 240η, που κατέληγε στη θέση της 230ης. Ακολούθως, οι τελευταίες δέκα γραμμές pixel, που αντιστοιχούν στην τελευταία γραμμή κειμένου (την 24η), διαγράφονταν. Μ’ αυτά και με τ’ άλλα, το πρόγραμμα τροποποιούσε όλα τα pixels της εικόνας. Όπως αντιλαμβάνεστε, οι σχετικές επεμβάσεις στη frame buffer καθυστερούσαν πάρα πολύ και προέκυπτε ένα φαινόμενο που είναι γνωστό σαν screen tearing. Ένα τμήμα της εικόνας εμφανιζόταν να έχει κυλίσει προς τα επάνω, ενώ η υπόλοιπη εμφανιζόταν στην αρχική της θέση. Μάλιστα, το πρόβλημα ήταν τόσο έντονο, που το “σπάσιμο” της εικόνας φαινόταν να ταξιδεύει σαν ένας κυματισμός!

Η ανάγνωση και η εγγραφή ολόκληρης της frame buffer απαιτεί εκ των πραγμάτων πολύ χρόνο και καμία βελτιστοποίηση δεν μπορούσε να σώσει την κατάσταση. Έπρεπε να βρούμε μια ριζικά διαφορετική λύση, η οποία θα απαιτούσε λιγότερες επεμβάσεις στην εικόνα. Η αλήθεια είναι ότι για κάμποσους μήνες παριστάναμε ότι η αργή κύλιση της εικόνας δεν μας ενοχλεί. Ωστόσο ήταν αδύνατο να ξεχάσουμε στ’ αλήθεια το πρόβλημα και σε μια αναλαμπή ευρηματικότητας σκεφτήκαμε το τέλειο τέχνασμα: Φανταστείτε την οθόνη σαν ένα παράθυρο, μέσα από το οποίο βλέπουμε μια περιοχή της μνήμης γραφικών. Η αρχική λύση για την κύλιση της εικόνας, προέβλεπε τη μετατόπιση όλων των δεδομένων της μνήμης. Η ιδανική λύση, όμως, θα ήταν να μετατοπίζαμε το παράθυρο!

Κάθε φορά που ενεργοποιείται ο υπολογιστής μας, εμφανίζεται το (πανέμορφο) λογότυπό του. Προφανώς, πρόκειται για μια εικόνα ενσωματωμένη στον κώδικα, που δεν παράγεται δυναμικά.

Κάθε φορά που ενεργοποιείται ο υπολογιστής μας, εμφανίζεται το (πανέμορφο) λογότυπό του. Προφανώς, πρόκειται για μια εικόνα ενσωματωμένη στον κώδικα, που δεν παράγεται δυναμικά.

Όπως είδαμε στο προηγούμενο άρθρο, η κατακόρυφη ανάλυση της εικόνας ανέρχεται στα 240 pixels. Ακριβώς γι’ αυτό, η ρουτίνα scanline στέλνει στην οθόνη το περιεχόμενο 240 γραμμών. Η αρχική εκδοχή της ρουτίνας scanline ξεκινούσε πάντα από την πρώτη γραμμή της frame buffer. Για να πετύχουμε το κολπάκι με το “παράθυρο” τροποποιήσαμε τη ρουτίνα κατάλληλα, ώστε να μην ξεκινά υποχρεωτικά από την πρώτη γραμμή. Πλέον, λίγο πριν ξεκινήσει το ορατό τμήμα ενός scanline, ο κώδικας προσθέτει στον αύξοντα αριθμό της γραμμής –στο high-byte του address bus– μια τιμή που λειτουργεί ως offset. Αναφερόμαστε στην τιμή της μεταβλητής sl_offset. Με αυτή τη μικρή τροποποίηση του προγράμματος, κάθε φορά που θέλουμε να κυλίσει η εικόνα κατά μία γραμμή κειμένου προς τα πάνω, αρκεί να αυξήσουμε την τιμή της sl_offset κατά δέκα. Το επόμενο frame θα σχηματιστεί από ένα διαφορετικό τμήμα της μνήμης οθόνης και η κύλιση της εικόνας θα πραγματοποιηθεί ακαριαία – για την ακρίβεια στο ένα εξηκοστό του δευτερολέπτου! Βέβαια, αμέσως μετά τη μετατόπιση της εικόνας πρέπει να καθαρίσουμε τις δέκα χαμηλότερες γραμμές pixels, που απαρτίζουν την τελευταία γραμμή κειμένου. Ωστόσο αυτή η επέμβαση στη μνήμη οθόνης ολοκληρώνεται σε ελάχιστο χρόνο και η κύλιση παραμένει ταχύτατη.

Κάπου εδώ μπορεί να προκύψει μια νέα απορία: Τι θα συμβεί όταν η τιμή της sl_offset ανέλθει, λόγου χάρη, στο 60, και η ρουτίνα scanline προσπαθήσει να διαβάσει την 200η γραμμή της εικόνας; Τίποτα το ανησυχητικό! Σε έναν οκτάμπιτο μικροελεγκτή, η πρόσθεση του 60 με το 200 έχει σαν αποτέλεσμα την ενεργοποίηση της σημαίας υπερχείλισης, ενώ παράγεται και ο αριθμός 4. Το πρόγραμμά μας αδιαφορεί πλήρως για την υπερχείλιση, κρατάει το 4 και αρχίζει να διαβάζει την τέταρτη γραμμή pixels της μνήμης οθόνης. Με άλλα λόγια, η ροή του προγράμματος δεν θα διαταραχθεί καθόλου. Βέβαια, η χρήση του offset έχει σαν αποτέλεσμα τη δέσμευση περισσότερου χώρου στη μνήμη οθόνης. Η frame buffer μοιάζει να κινείται μέσα στη μνήμη οθόνης κι απαιτεί μια έκταση που χωράει 256 γραμμές pixels και όχι 240. Λαμβάνοντας υπόψη ότι ακόμα κι έτσι περισσεύει άφθονος χώρος στο τσιπάκι μνήμης, αυτό το τίμημα κρίθηκε αμελητέο.

Μετατροπές χρωμάτων
Κάθε φορά που ενεργοποιείται ο υπολογιστής μας η οθόνη προβάλει ένα μήνυμα καλωσορίσματος και ένα μικρό λογότυπο (deltaPC). Όπως αντιλαμβάνεστε, αυτή η εικόνα δεν παράγεται δυναμικά από το κύκλωμα γραφικών. Τη φτιάξαμε σ’ ένα σχεδιαστικό πρόγραμμα του “μεγάλου” υπολογιστή και την ενσωματώσαμε στο firmware της GPU. Για την ακρίβεια, τα δεδομένα της εικόνας αποθηκεύτηκαν σε έναν πίνακα, ενώ προσθέσαμε στο πρόγραμμα και μια ρουτίνα που αντιγράφει το περιεχόμενο του συγκεκριμένου πίνακα στο “κέντρο” της frame buffer. Όλα αυτά είναι απλά και κατανοητά, αλλά κρύβουν ένα ενδιαφέρον ζήτημα: Το δικό μας κύκλωμα γραφικών χρησιμοποιεί ένα color format που δεν υπάρχει στα σύγχρονα σχεδιαστικά προγράμματα. Πώς πραγματοποιήσαμε την απαραίτητη μετατροπή;

Το λογότυπο του υπολογιστή μας, όπως φαίνεται στο πρόγραμμα ζωγραφικής των Windows. Παρατηρείστε τη ρύθμιση της μεγέθυνσης που έχει τεθεί στο 800%!

Το λογότυπο του υπολογιστή μας, όπως φαίνεται στο πρόγραμμα ζωγραφικής των Windows. Παρατηρείστε τη ρύθμιση της μεγέθυνσης που έχει τεθεί στο 800%!

Όταν σχεδιάσαμε το λογότυπο, το αποθηκεύσαμε σαν BMP με βάθος χρώματος 24bit. Επειδή οι εικόνες αυτού του τύπου δεν περιλαμβάνουν διαφάνειες (απουσιάζει το λεγόμενο alpha channel), τα 24 bits κατανέμονται αποκλειστικά στις τρεις χρωματικές συνιστώσες. Συνεπώς, για κάθε pixel του λογότυπου το αρχείο διέθετε 8 bits για το κόκκινο, 8 bits για το πράσινο και άλλα τόσα για το μπλε. Αυτό ήταν αρκετά βολικό, αλλά δεν έλυνε το πρόβλημα. Στο δικό μας κύκλωμα γραφικών, κάθε pixel περιγράφεται μόνο από 8 bits. Όπως θα θυμόσαστε, τo πρώτο ζεύγος bits χρησιμοποιείται για το κόκκινο, το επόμενο ζεύγος για το πράσινο και το τρίτο για το μπλε. Τα τελευταία δύο bits ρυθμίζουν την ένταση όλων των χρωματικών συνιστωσών κι ως εκ τούτου περιγράφουν τη φωτεινότητα. Δεν ξέρουμε αν το συνειδητοποιείτε, αλλά η μετατροπή μεταξύ των συγκεκριμένων color formats είναι απλούστατη. Αφενός, τα δύο bits που προβλέπει το δικό μας πρότυπο για κάθε χρωματική συνιστώσα προκύπτουν ευθέως από τα 2 περισσότερο σημαντικά bits των αντίστοιχων οκτάδων του BMP. Αφετέρου, τα δύο bits φωτεινότητας του δικού μας προτύπου προκύπτουν από τα 2 περισσότερο σημαντικά bits της φωτεινότητας στο BMP. Αναρωτιέστε πού θα βρούμε τη φωτεινότητα των pixels μέσα στο BMP; Η αλήθεια είναι ότι δεν θα τη βρούμε πουθενά, αλλά μπορούμε να την υπολογίσουμε πανεύκολα. Αρκεί να σας πούμε ότι προκύπτει από το μέσο όρο των τιμών των τριών χρωμάτων. Τελικά, η διαδικασία της μετατροπής περιλαμβάνει μερικές απλές πράξεις, οι οποίες ωστόσο θα πρέπει να επαναληφθούν για κάθε pixel. Κάπως έτσι, επειδή απεχθανόμαστε τις χαμαλοδουλειές, βρήκαμε μια πρώτης τάξεως ευκαιρία για να καταφύγουμε στην αγαπημένη μας Python. Μπορείτε να κατεβάσετε το πρόγραμμα που γράψαμε από εδώ. Για την εύκολη ανάγνωση των δεδομένων της εικόνας χρησιμοποιήσαμε μια κλάση της βιβλιοθήκης PIL (Python Image Library). Σημειώστε, λοιπόν, ότι το πρόγραμμά μας προϋποθέτει την ύπαρξη της συγκεκριμένης βιβλιοθήκης. Ο τρόπος χρήσης του είναι εξαιρετικά απλός. Για κάθε εικόνα που του δίνουμε ως παράμετρο, δημιουργεί έναν πίνακα που μπορούμε να ενσωματώσουμε απευθείας στο firmware της GPU. Από εκεί και πέρα, αναλαμβάνει το πρόγραμμα και συγκεκριμένα μια ρουτίνα που αντιγράφει τα περιεχόμενα του πίνακα στη frame buffer.

Το αλατοπίπερο
Κάπου εδώ θα μπορούσαμε να σταματήσουμε. Παρουσιάσαμε όλα τα τμήματα του κώδικα κι έχετε μια πλήρη εικόνα της διάρθρωσης και της λειτουργίας του. Ωστόσο, αν η παρουσίαση τελείωνε εδώ, θα αποκομίζατε μια λανθασμένη αντίληψη για την ανάπτυξη του προγράμματος. Στην πορεία συναντήσαμε πάρα πολλά προβλήματα, τα περισσότερα από τα οποία δεν είχαν καμία σχέση με τις ενδιαφέρουσες προκλήσεις που μελετήσαμε. Αναφερόμαστε σε εκνευριστικά bugs που δοκίμασαν την υπομονή μας και σχετίζονταν αποκλειστικά με τη χρήση της γλώσσας Assembly. Για του λόγου το αληθές, στη συνέχεια θα παρουσιάσουμε το πιο ύπουλο σφάλμα στον κώδικα.

Το προγραμματάκι που γράψαμε σε Python δέχεται σαν παράμετρο την εικόνα που θέλουμε να μετατρέψουμε και παράγει ένα αρχείο απλού κειμένου. Εκεί μέσα τοποθετείται ο πίνακας που πρέπει να ενσωματώσουμε στο firmware της GPU, για την προβολή της εικόνας από το κύκλωμα γραφικών.

Το προγραμματάκι που γράψαμε σε Python δέχεται σαν παράμετρο την εικόνα που θέλουμε να μετατρέψουμε και παράγει ένα αρχείο απλού κειμένου. Εκεί μέσα τοποθετείται ο πίνακας που πρέπει να ενσωματώσουμε στο firmware της GPU, για την προβολή της εικόνας από το κύκλωμα γραφικών.

Όταν η οθόνη άρχισε να αναγνωρίζει το σήμα που παρήγαγε το πρόγραμμά μας, ξεκινήσαμε μια σειρά ενθουσιωδών δοκιμών. Βέβαια, σε εκείνη τη φάση δεν είχαμε υλοποιήσει τις ρουτίνες επικοινωνίας και το κύκλωμα γραφικών δεν μπορούσε να δεχτεί εντολές από το περιβάλλον του. Έτσι, τα δοκιμαστικά μηνύματα που τυπώναμε στην οθόνη –σε διάφορες θέσεις και με ποικίλα χρώματα–, δημιουργούνταν με πρωτοβουλία της ίδιας της GPU. Αν κι επιβεβλημένος, αυτός ο τρόπος δοκιμής του σήματος ήταν ο πλέον κατάλληλος, αφού απομόνωνε όλους τους εξωτερικούς παράγοντες (προβλήματα στην επικοινωνία, σφάλματα στον κώδικα άλλων συστημάτων κ.ά.). Δυστυχώς όμως ο αρχικός ενθουσιασμός δεν κράτησε πολύ. Στη δεύτερη ή τρίτη δοκιμή ήρθαμε αντιμέτωποι με ένα μυστήριο bug: Μερικές γραμμές pixels, λίγων μόνο χαρακτήρων, εκτείνονταν σε όλη τη γραμμή της οθόνης και είχαν απρόβλεπτο περιεχόμενο. Προσπαθώντας να συσχετίσουμε αυτό το παράξενο bug με κάποιο γνωστό μέγεθος, στην επόμενη έκδοση του κώδικα προσθέσαμε περισσότερα δοκιμαστικά μηνύματα, σε διάφορες θέσεις της οθόνης. Το αποτέλεσμα ήταν άκρως απογοητευτικό! Εκτός από το προηγούμενο bug εμφανίστηκε κι ένα νέο: Κάτω από μερικά γράμματα και ψηφία, τυπώνονταν όσοι χαρακτήρες βρίσκονται στις επόμενες θέσεις του πίνακα ASCII. Για παράδειγμα, κάτω από μια εμφάνιση του ψηφίου 9 (όχι όλες!) εμφανίζονταν οι χαρακτήρες :, ;, <, =, >, ?, @, A, B, C κ.ο.κ., μέχρι το z!

Τα δύο προβλήματα παρέπεμπαν στην ρουτίνα εμφάνισης των χαρακτήρων. Ήταν φανερό ότι κάποιοι μετρητές ξέφευγαν από τα προβλεπόμενα όρια και ο αντίστοιχος βρόχος επαναλήψεων λειτουργούσε ανεξέλεγκτα. Μόνο κάτι τέτοιο θα μπορούσε να δικαιολογήσει την προβολή περισσότερων pixels ή χαρακτήρων. Περιττό να πούμε ότι ακολούθησε μια εξονυχιστική διερεύνηση του κώδικα, η οποία ωστόσο αποδείχθηκε άκαρπη. Οι έλεγχοι στις τιμές των διαφόρων μετρητών (boundary checks) πραγματοποιούνταν ολόσωστα και δεν υπήρχε τίποτα που να δικαιολογεί την αλλοπρόσαλλη συμπεριφορά των βρόχων. Με τα πολλά, καταλήξαμε να ψάχνουμε σε μια φαινομενικά άσχετη θέση: Στον κώδικα της ρουτίνας scanline. Όπως ξέρουμε, η συγκεκριμένη ρουτίνα καλείται με ένα interrupt κι ορισμένες φορές η εκτέλεσή της συνέπιπτε με κάποιον έλεγχο. Προσέξτε τώρα το εξής: Στην Assembly, ακόμα και οι απλούστεροι έλεγχοι πραγματοποιούνται σε δύο βήματα. Για παράδειγμα, για να τσεκάρουμε αν ένας καταχωρητής έχει τιμή μεγαλύτερη ή μικρότερη του 47, κάνουμε τα εξής: Αφαιρούμε την τιμή 47 από την τιμή του καταχωρητή και ακολούθως ελέγχουμε αν το αποτέλεσμα της πράξης ήταν αρνητικό ή θετικό. Ωραία, θα πείτε, και πώς ελέγχουμε το αποτέλεσμα της αφαίρεσης; Αυτός ο έλεγχος πραγματοποιείται πανεύκολα, τσεκάροντας ένα συγκεκριμένο bit του καταχωρητή SREG. Ο εν λόγω καταχωρητής αντικατοπτρίζει την κατάσταση του μικροελεγκτή και, μεταξύ άλλων, περιγράφει το αποτέλεσμα της πιο πρόσφατης πράξης. Κάποιες φορές, λοιπόν, η ρουτίνα scanline ξεκινούσε αμέσως μετά την δοκιμαστική αφαίρεση κι ακριβώς πριν από τον έλεγχο του SREG. Μήπως πηγαίνει το μυαλό σας πουθενά; Μέσα στη ρουτίνα scanline πραγματοποιούνται πολλές πράξεις και η τιμή του SREG μεταβάλλεται. Επομένως, όταν η ροή του προγράμματος επιστρέφει σε κάποιον έλεγχο που είχε κοπεί στη μέση, η τιμή του SREG έχει πλέον αλλοιωθεί και η έκβαση του ελέγχου είναι πρακτικά τυχαία. Αυτό το bug είναι αρκετά ύπουλο, αλλά δεν μας είχε ξεφύγει. Το δικό μας λάθος ήταν προϊόν μεγαλύτερης απροσεξίας!

Γράφοντας τη ρουτίνα scanline, δεν είχαμε παραβλέψει το ενδεχόμενο του να διακόψει κάποιον έλεγχο. Έτσι, είχαμε μεριμνήσει για την αποθήκευση του SREG και φυσικά για την επαναφορά του. Δείτε πώς ξεκινούσε η ρουτίνα scanline:

cbi PORTB, hsync          ; (2) start horizontal sync pulse
in isr_temp1, PORTB       ; (1) save control signals
sbr isr_temp1, (1<<hsync) ; (1) restore initial hsync state
mov r3, isr_temp1         ; (1)
in r10, SREG              ; (1) save SREG	

Ο παλμός HSYNC βρίσκεται στην έναρξη της γραμμής κι ακριβώς γι’ αυτό, ο κώδικας ξεκινά με την πρώτη φάση του παλμού: Μετάβαση από HIGH σε LOW. Αμέσως μετά αποθηκεύουμε την κατάσταση των ακροδεκτών του PORTB. Σε αυτούς τους ακροδέκτες, όμως, συγκαταλέγεται κι εκείνος στον οποίο δημιουργείται ο παλμός HSYNC. Επομένως, για να αποθηκεύσουμε την αρχική, αναλλοίωτη κατάσταση των ακροδεκτών, δημιουργούμε ένα αντίγραφο των τιμών τους (isr_temp1) κι εκεί φροντίζουμε να επαναφέρουμε τον ακροδέκτη του παλμού HSYNC στην πρότερη κατάσταση. Κατόπιν αποθηκεύουμε την τιμή του isr_temp1, όπως και αυτήν του SREG. Μπορείτε να δείτε το λάθος;

Στο απόσπασμα που μελετάμε, οι εντολές cbi και sbr πετυχαίνουν κάτι παραπλήσιο: Αλλάζουν την τιμή ενός bit, σε κάποιον καταχωρητή. Αυτή η ομοιότητα είναι παραπλανητική! Οι εντολές cbi και sbr λειτουργούν πολύ διαφορετικά. Η πρώτη επιδρά μόνο σε ένα bit (το θέτει σε LOW), ενώ η δεύτερη μπορεί να επιδράσει ταυτόχρονα σε πολλά (τα θέτει σε HIGH). Η δεύτερη πετυχαίνει το σκοπό της πραγματοποιώντας ένα λογικό OR, μεταξύ του καταχωρητή και της τιμής που έχουμε δηλώσει. Ως εκ τούτου, ενώ η εκτέλεση της εντολής cbi δεν τροποποιεί ποτέ τον καταχωρητή SREG, η εκτέλεση της sbr τον επηρεάζει πάντα. Τελικά, για να διορθώσουμε το bug μετατοπίσαμε μια γραμμή κώδικα:

cbi PORTB, hsync          ; (2) start horizontal sync pulse
in r10, SREG              ; (1) save SREG --- before execution of sbr!
in isr_temp1, PORTB       ; (1) save control signals
sbr isr_temp1, (1<<hsync) ; (1) restore initial hsync state
mov r3, isr_temp1         ; (1)

Χρειάζεται να πούμε κάτι περισσότερο; Η Assembly προσφέρει τον υπέρτατο έλεγχο οποιουδήποτε μικροελεγκτή κι ενώ αυτό μοιάζει με προτέρημα, μπορεί άνετα να μετατραπεί σε εφιάλτη. Σκεφτείτε ότι πατήσαμε πολλές μπανανόφλουδες σαν αυτή και κάθε φορά ταλαιπωρούμασταν για μέρες. Ευτυχώς που η κατασκευή ενός υπολογιστή αποτελεί μακράν τον πιο συναρπαστικό στόχο, γιατί θα είχαμε παραιτηθεί από πολύ νωρίς.

Leave a Reply

You must be logged in to post a comment.

Σύνδεση

Αρχείο δημοσιεύσεων