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

Κατασκευάστε έναν υπολογιστή, μέρος 3: Pixel προς pixel!

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

Στο προηγούμενο μέρος της σειράς μας μελετήσαμε τα κυκλώματα που απαρτίζουν τον υπολογιστή μας. Αυτό δεν σημαίνει ότι ξεμπερδέψαμε με την κατασκευή. Ο δικός μας υπολογιστής δεν χρησιμοποιεί εξειδικευμένα τσιπ για την εικόνα και τον ήχο. Το ίδιο ισχύει και για τον κεντρικό επεξεργαστή, ο οποίος λειτουργεί ως interpreter της BASIC και ταυτόχρονα ως ένα υπεραπλουστευμένο “τσίπσετ”. Σε κάθε περίπτωση έχουμε επιστρατεύσει έναν μικροελεγκτή της οικογένειας ATMEGA. Αυτό σημαίνει ότι ο υπολογιστής μας δεν μπορεί να ενεργοποιηθεί αμέσως μετά τη συναρμολόγηση των κυκλωμάτων. Για να λειτουργήσει κάθε υποσύστημα με τον προβλεπόμενο τρόπο, πρέπει πρώτα να προγραμματίσουμε κατάλληλα τους μικροελεγκτές.

Σε αυτό το άρθρο θα ξεκινήσουμε με το firmware της “κάρτας γραφικών”. Εξετάζοντας το σχετικό κύκλωμα, αναφέραμε ότι τα δεδομένα που συνθέτουν την εικόνα αποθηκεύονται σ’ ένα τσιπάκι μνήμης (frame buffer). Είδαμε επίσης ότι στο ρόλο της GPU βρίσκεται ένας ATMEGA644. Ε, λοιπόν, το πρόγραμμα που γράψαμε για αυτόν τον μικροελεγκτή αξιοποιεί τα δεδομένα του frame buffer και σχηματίζει το σήμα που οδηγεί την οθόνη. Αργότερα θα δούμε ότι το ίδιο πρόγραμμα εκτελεί πολλές ακόμα λειτουργίες, αλλά η παραγωγή του σήματος εικόνας είναι μακράν η κρισιμότερη. Το σήμα που παράγεται είναι τύπου VGA και το προτιμήσαμε για δύο λόγους. Αφενός, τα υπόλοιπα αναλογικά σήματα είναι αρκετά παλιά κι έχουν αρχίσει να εκλείπουν από τις σύγχρονες συσκευές. Αν επιλέγαμε κάτι σε CGA, EGA ή ακόμα και Composite, θα περιορίζαμε δραστικά το πλήθος των συμβατών οθονών. Αφετέρου, τα ψηφιακά σήματα απαιτούν αρκετά ισχυρότερο υλικό και η παραγωγή τους αποτελεί άπιαστο όνειρο για τους 8μπιτους AVR. Την ίδια στιγμή, όλες οι οθόνες και οι τηλεοράσεις των τελευταίων δεκαετιών διαθέτουν τουλάχιστον μια είσοδο VGA.

Εξελικτικό απομεινάρι
Σας λέει τίποτα η λέξη “πυροβόλο”, εκτός από το γνωστό όπλο; Τι καταλαβαίνετε όταν ακούτε ότι “πρέπει να απομαγνητίσουμε τα πηνία”; Αν νομίζετε ότι ο γράφων άρχισε να παραμιλά, φταίει που δεν εξοικειωθήκατε ποτέ με τις οθόνες CRT. Το όνομά τους προκύπτει από το σπουδαιότερο εξάρτημα του μηχανισμού που σχημάτιζε την εικόνα. Αναφερόμαστε στην καθοδική λυχνία (Cathode Ray Tube) που οι παλιότεροι ονόμαζαν και “πυροβόλο”. Το συγκεκριμένο εξάρτημα εξέπεμπε μια δέσμη ηλεκτρονίων, η οποία μπορούσε να εκτραπεί οριζοντίως και καθέτως με τη βοήθεια τεσσάρων πηνίων (δύο για την οριζόντια εκτροπή και δύο για την κάθετη). Η εν λόγω δέσμη λειτουργούσε σαν ένα λεπτεπίλεπτο πινέλο, που σάρωνε την επιφάνεια της οθόνης και σχημάτιζε την εικόνα. Η σάρωση ξεκινούσε από την πάνω αριστερή γωνία (όπως κοιτάμε εμείς την οθόνη), ακολουθούσε μια πορεία ζιγκ-ζαγκ κι έφτανε μέχρι την κάτω δεξιά γωνία. Με αυτόν τον τρόπο η εικόνα σχηματιζόταν σταδιακά, γραμμή προς γραμμή. Με την ευκαιρία, σημειώστε ότι κάθε τέτοια γραμμή ονομάζεται scanline, ενώ το σύνολο των γραμμών ονομάζεται frame ή field. Φυσικά, όλα τα παραπάνω εξελίσσονταν με μεγάλη ταχύτητα και δεν γίνονταν αντιληπτά από τις αισθήσεις μας. Αν εξαιρέσουμε ένα αμυδρό τρεμόπαιγμα, που κι αυτό δεν εντοπιζόταν πάντα, τα μάτια μας έβλεπαν μόνο την ολοκληρωμένη εικόνα. Μήπως αναρωτιέστε τι μας έπιασε τώρα και το ρίξαμε στην αρχαιολογία; Ο τρόπος λειτουργίας των παλιών τηλεοράσεων καθόρισε σε μεγάλο βαθμό την “εσωτερική δομή” των αναλογικών σημάτων εικόνας: Όλα τα σήματα του είδους περιλαμβάνουν έναν παλμό κατακόρυφου συγχρονισμού που σηματοδοτεί την έναρξη των frames, έναν παλμό οριζόντιου συγχρονισμού που σηματοδοτεί την εκκίνηση των scanlines και, στο ενδιάμεσο, τη χρωματική πληροφορία για τα διαδοχικά pixels που απαρτίζουν την εκάστοτε γραμμή.

Στο πίσω άκρο της οθόνης φαίνεται η καθοδική λυχνία (Cathode Ray Tube), που παρήγαγε τη δέσμη ηλεκτρονίων. Λίγο πιο μπροστά διακρίνονται τα πηνία της κατακόρυφης και οριζόντιας εκτροπής της δέσμης. Αυτός ο πρωτόγονος μηχανισμός έχει αφήσει τη στάμπα του σε όλα τα αναλογικά σήματα εικόνας.

Στο πίσω άκρο της οθόνης φαίνεται η καθοδική λυχνία (Cathode Ray Tube), που παρήγαγε τη δέσμη ηλεκτρονίων. Λίγο πιο μπροστά διακρίνονται τα πηνία της κατακόρυφης και οριζόντιας εκτροπής της δέσμης. Αυτός ο πρωτόγονος μηχανισμός έχει αφήσει τη στάμπα του σε όλα τα αναλογικά σήματα εικόνας.

Λαβύρινθος προδιαγραφών
Αν και τα κυκλώματα VGA (Video Graphics Array) εμφανίστηκαν για πρώτη φορά το 1987, στους υπολογιστές IBM PS/2, το όνομα VGA χρησιμοποιείται ευρύτατα μέχρι σήμερα και κατέληξε να σημαίνει πολλά διαφορετικά πράγματα. Για παράδειγμα, VGA ονομάζεται το αναλογικό σήμα εικόνας που παράγουν οι κάρτες γραφικών, όπως επίσης και η υποδοχή D-sub 15 που έχει καθιερωθεί για τη μεταφορά του εν λόγω σήματος. Εξάλλου, με το ίδιο όνομα γίνεται λόγος και για τις ίδιες τις κάρτες γραφικών. Το μεγάλο μπέρδεμα όμως σχετίζεται με τις αναλύσεις. Το όνομα VGA αναφέρεται και στην ανάλυση που προσέφεραν τα πρώτα κυκλώματα του είδους (640 x 480 pixels). Για τις υπόλοιπες αναλύσεις –ακόμα κι όταν μεταφέρονται με αναλογικό σήμα VGA–, χρησιμοποιούνται διαφορετικά ονόματα (SVGA, SXGA, WUXGA, κ.λπ.). Στη δική μας κατασκευή, πάντως, αποφασίσαμε να υιοθετήσουμε την παραδοσιακή ερμηνεία του όρου. Έτσι, το κύκλωμα γραφικών του υπολογιστή μας παράγει ένα σήμα, που έχει τα ίδια βασικά χαρακτηριστικά με το σήμα των πρώτων κυκλωμάτων VGA.

Όπως όλα τα αναλογικά σήματα, το σήμα VGA περιλαμβάνει έναν παλμό με το όνομα VSYNC (κατακόρυφος συγχρονισμός) και έναν παλμό ονόματι HSYNC (οριζόντιος συγχρονισμός). Επιπρόσθετα, καθώς το πρότυπο VGA προβλέπει τη χρήση χρωμάτων, υπάρχουν τρεις ακόμα “γραμμές”, που μεταφέρουν την πληροφορία για κάθε χρωματική συνιστώσα (Red, Green και Blue). Τα σήματα VSYNC και HSYNC είναι ψηφιακά και “ανεστραμμένης” ή “αρνητικής” λογικής. Αυτό σημαίνει ότι βρίσκονται μονίμως στη λογική κατάσταση “HIGH” και μεταβαίνουν στην κατάσταση “LOW” προσωρινά, για όσο χρόνο είναι ενεργοί οι αντίστοιχοι παλμοί. Τα άλλα τρία σήματα είναι αναλογικά και η τάση του καθενός κυμαίνεται στο διάστημα από 0 έως 0,7Volt. Το άνω άκρο ερμηνεύεται ως πλήρη φωτεινότητα για την εκάστοτε συνιστώσα, ενώ το κάτω ερμηνεύεται ως μηδενική φωτεινότητα. Η παραπάνω περιγραφή είναι απόλυτα ακριβής και ισχύει για όλα τα σήματα VGA, αλλά δεν αρκεί για την υλοποίησή τους. Για τη σύνθεση ενός τέτοιου σήματος πρέπει να γνωρίζουμε τα λεγόμενα timings. Με απλά λόγια, πρέπει να γνωρίζουμε πόσο διαρκεί κάθε παλμός και με ποια συχνότητα επαναλαμβάνεται, όπως επίσης και το χρόνο που εκπέμπεται η χρωματική πληροφορία για κάθε pixel. Όλα αυτά τα μεγέθη καθορίζονται από το ρυθμό ανανέωσης της εικόνας και, προφανώς, από την ανάλυση.

Ο παλμός οριζόντιου συγχρονισμού επαναλαμβάνεται στην εκκίνηση κάθε scanline. Ο παλμός κατακόρυφου συγχρονισμού επαναλαμβάνεται ανά 525 scanlines (στην εκκίνηση κάθε frame) και διαρκεί όσο δύο ολόκληρα scanlines.

Ο παλμός οριζόντιου συγχρονισμού επαναλαμβάνεται στην εκκίνηση κάθε scanline. Ο παλμός κατακόρυφου συγχρονισμού επαναλαμβάνεται ανά 525 scanlines (στην εκκίνηση κάθε frame) και διαρκεί όσο δύο ολόκληρα scanlines.

Η τυπική ανάλυση VGA ανέρχεται στα 640 x 480 pixels και ο αντίστοιχος ρυθμός ανανέωσής της (refresh rate) φτάνει στα 59,94Hz. Αυτό σημαίνει ότι τα δεδομένα που συνθέτουν την εικόνα αποστέλλονται στην οθόνη 59,94 φορές το δευτερόλεπτο και, συνεπώς, ο παλμός VSYNC επαναλαμβάνεται με συχνότητα 59,94Hz. Μη βιαστείτε να κάνετε διαιρέσεις, ώστε να υπολογίσετε τους χρόνους των υπολοίπων σημάτων. Υπάρχει μια λεπτομέρεια στη δομή του σήματος VGA που θα μπορούσε άνετα να θεωρηθεί ως παγίδα και πηγάζει (από πού αλλού;) από την τεχνολογία των οθονών CRT. Επειδή η συμπεριφορά των πηνίων παρουσιάζει αδράνεια, κάθε φορά που η δέσμη ηλεκτρονίων έφτανε στο τέλος μιας γραμμής δεν μπορούσε να φρενάρει και να επιστρέψει ακαριαία στη θέση εκκίνησης της επόμενης γραμμής. Το ίδιο ίσχυε και στον τερματισμό της σάρωσης του frame (κάτω δεξιά), οπότε η δέσμη έπρεπε να επιστρέψει στο σημείο έναρξης (πάνω αριστερά). Αυτή η αλλαγή στην κατεύθυνση κίνησης, όπως και η ίδια η μετατόπιση, απαιτούσε κάποιο χρόνο. Ακριβώς γι’ αυτό, το σήμα VGA με ανάλυση 640 x 480 περιλαμβάνει 525 scanlines και όχι 480, όπως θα περίμενε κανείς. Οι πρόσθετες γραμμές ορίζουν το λεγόμενο vertical video blanking interval και δεν περιλαμβάνουν κάποια ορατή πληροφορία για την εικόνα. (Στην περίπτωση του αναλογικού τηλεοπτικού σήματος, οι συγκεκριμένες γραμμές χρησιμοποιούνταν για την εκπομπή κάποιων metadata.) Η ύπαρξη αυτών των γραμμών αποσκοπεί μόνο στην εισαγωγή μιας μικρής καθυστέρησης, ώστε να επιστρέψει η δέσμη ηλεκτρονίων στη θέση έναρξης του frame. Αντίστοιχα, κάθε scanline περιλαμβάνει 800 pixel και όχι 640. Τα πρόσθετα pixel δεν είναι ορατά και ορίζουν το λεγόμενο horizontal video blanking interval. Η παρουσία τους επιτρέπει στη δέσμη ηλεκτρονίων να μεταβεί στη θέση εκκίνησης της επόμενης γραμμής. Αυτά τα “νεκρά” χρονικά διαστήματα χωρίζονται σε δύο επιμέρους τμήματα και έχουν τα ονόματα back porch και front porch. Το back porch, σε κάθε scanline, βρίσκεται ακριβώς πριν από τα 640 ορατά pixels, ενώ το front porch ακολουθεί. Αντίστοιχα, το back porch σε κάθε frame βρίσκεται πριν από τις 480 ορατές γραμμές, ενώ το front porch ακολουθεί. Τελικά, το κλασικό σήμα VGA –αυτό που παράγει και το δικό μας κύκλωμα γραφικών– έχει τα χαρακτηριστικά που φαίνονται στους ακόλουθους πίνακες.

Τα χαρακτηριστικά του σήματος VGA που παράγει το δικό μας κύκλωμα γραφικών.

Τα χαρακτηριστικά του σήματος VGA που παράγει το δικό μας κύκλωμα γραφικών.

Αν κάνετε τις πράξεις θα καταλήξετε στα εξής: Ο χρόνος για κάθε pixel αγγίζει μόλις τα 39,72nsec, που σημαίνει ότι η συχνότητα μετάδοσης των δεδομένων των pixel (pixel clock) ανέρχεται στα 25,175MHz. Αυτά τα μεγέθη, όπως και όσα περιλαμβάνονται στους δύο πίνακες, δεν μας απασχόλησαν στο πλαίσιο κάποιας αριθμολαγνείας. Το πρόγραμμά μας έπρεπε να υλοποιεί το σήμα VGA με τεράστια ακρίβεια, αφού όλες οι οθόνες –και πολύ περισσότερο οι παλιές– είναι ιδιαίτερα αυστηρές με τους χρόνους. Όταν το σήμα αποκλίνει έστω και λίγο από το εκάστοτε πρότυπο, η οθόνη αρνείται να “κλειδώσει” και δεν δείχνει απολύτως τίποτα.

Ο παλμός HSYNC σηματοδοτεί την εκκίνηση ενός scanline (A). Αμέσως μετά ακολουθεί το back porch (B), ενώ στο τέλος του scanline βρίσκεται το front porch (D). Ανάμεσα σ' αυτά τα δύο βρίσκεται το ορατό τμήμα της γραμμής (C). Οι τρείς τάσεις περιγράφουν τα χρώματα των pixels που απαρτίζουν τη γραμμή.

Ο παλμός HSYNC σηματοδοτεί την εκκίνηση ενός scanline (A). Αμέσως μετά ακολουθεί το back porch (B), ενώ στο τέλος του scanline βρίσκεται το front porch (D). Ανάμεσα σ’ αυτά τα δύο βρίσκεται το ορατό τμήμα της γραμμής (C). Οι τρείς τάσεις περιγράφουν τα χρώματα των pixels που απαρτίζουν τη γραμμή.

Θέμα χρόνου
Η δημιουργία των παλμών HSYCN και VSYNC αποτελεί απλή υπόθεση, αφού οι συχνότητές τους (31,469KHz και 59,94Hz αντίστοιχα) είναι πολύ χαμηλές για τα δεδομένα ενός μικροελεγκτή. Όσο πρόχειρα κι αν γράφαμε τον κώδικα, είναι σίγουρο ότι δεν θα δυσκολευόμασταν καθόλου. Τα χρώματα των διαδοχικών pixels, όμως, πρέπει να μεταδίδονται με το ρυθμό που καθορίζει το pixel clock. Αυτό σημαίνει ότι τα δεδομένα της frame buffer πρέπει να εξέρχονται από αυτή με τη συχνότητα των 25,175MHz. Αν ρίξετε μια ματιά στο datasheet της μνήμης (TC551001BPL-70L), θα διαπιστώσετε ότι ο ελάχιστος χρόνος ανάγνωσης (minimum read access time) φτάνει στα 70nsec. Συνεπώς, ο μέγιστος ρυθμός ανάγνωσης προσεγγίζει μόλις τα 14,286MHz. Μήπως η επιλογή αυτής της μνήμης συνιστά σχεδιαστικό σφάλμα; Τα ψηφιακά κυκλώματα κρύβουν πολλές παγίδες σαν αυτή, αλλά να είστε σίγουροι ότι εδώ δεν έγινε λάθος. Ακόμα κι αν είχαμε χρησιμοποιήσει γρηγορότερη μνήμη, δεν θα κερδίζαμε απολύτως τίποτα. Το κύκλωμά μας περιλαμβάνει έναν ακόμα πιο αργό “παίκτη”. Αναφερόμαστε στον μικροελεγκτή, που σύμφωνα με το datasheet έχει μέγιστη συχνότητα λειτουργίας τα 20MHz. Αυτό ακούγεται πολύ καλό, αλλά δεν αρκεί. Θυμηθείτε για λίγο το κύκλωμά μας. Το address bus της μνήμης καταλήγει στον μικροελεγκτή. Αυτό σημαίνει ότι για να διαβάσουμε από τη frame buffer τις τιμές μερικών pixels, ο μικροελεγκτής πρέπει να υπολογίσει και να “ζητήσει” τις κατάλληλες διευθύνσεις μνήμης. Στην καλύτερη περίπτωση, αυτό προϋποθέτει μία (!) εντολή για τον υπολογισμό της διεύθυνσης και άλλη μία για την εμφάνισή της στους ακροδέκτες. Αν ο μικροελεγκτής λειτουργεί στη μέγιστη συχνότητα των 20MHz και αν οι παραπάνω δύο εντολές εκτελούνται σε ένα κύκλο του ρολογιού (CPU cycle) η καθεμία, οι νέες διευθύνσεις μνήμης θα εμφανίζονται στους ακροδέκτες του μικροελεγκτή με συχνότητα 10MHz. Κατ’ επέκταση, η μέγιστη ταχύτητα ανάγνωσης μερικών τιμών από τη frame buffer φτάνει το πολύ στα 10MHz. Εδώ συναντάμε ένα πραγματικό εμπόδιο! Το pixel clock των 25,175MHz είναι άπιαστο για το κύκλωμα γραφικών του υπολογιστή μας. Ούτε η μνήμη, ούτε και ο μικροελεγκτής θα μπορούν να εκπέμψουν τα δεδομένα για 640 pixels, μέσα στο προβλεπόμενο χρονικό διάστημα.

Η αναπαράσταση ενός frame στο επίπεδο. Η πράσινη περιοχή αποτελεί το ορατό τμήμα του frame. Οι περιοχές A και B αποτελούν τα τμήματα του horizontal video blanking, ενώ οι περιοχές C και D αντιστοιχούν στα τμήματα του vertical video blanking.

Η αναπαράσταση ενός frame στο επίπεδο. Η πράσινη περιοχή αποτελεί το ορατό τμήμα του frame. Οι περιοχές A και B αποτελούν τα τμήματα του horizontal video blanking, ενώ οι περιοχές C και D αντιστοιχούν στα τμήματα του vertical video blanking.

Αναγκαίος συμβιβασμός
Οι οθόνες δεν ενδιαφέρονται καθόλου για το περιεχόμενο των εικόνων που προβάλλουν. Αυτό που τις ενδιαφέρει είναι να λαμβάνουν τους παλμούς συγχρονισμού (VSYNC και HSYNC) στους σωστούς χρόνους και, επιπρόσθετα, να λαμβάνουν τα δεδομένα των ορατών pixels μέσα στα κατάλληλα χρονικά διαστήματα. Το αν θα επαναλαμβάνονται κάποια pixels ή θα είναι όλα διαφορετικά, δεν παίζει κανένα ρόλο. Πίσω απ’ αυτή την τετριμμένη αλήθεια κρύβεται η λύση του προβλήματός μας. Είτε στέλνουμε δεδομένα για 640 pixels είτε για λιγότερα, η οθόνη δεν θα ενοχληθεί καθόλου. Αρκεί η εκπομπή των δεδομένων να διαρκεί ακριβώς όσο προβλέπεται από το πρότυπο. Αυτό σημαίνει ότι αν κάνουμε μια υποχώρηση στην ανάλυση της εικόνας, το πρόβλημα με το pixel clock λύνεται αυτομάτως. Ωραία, θα σκεφτείτε, αλλά πόσα pixels προλαβαίνουμε να στείλουμε στο χρονικό διάστημα που η οθόνη περιμένει 640; Όπως είδαμε, το pixel clock του δικού μας κυκλώματος μπορεί να ανέλθει το πολύ ως τα 10MHz. Με αυτό το δεδομένο, η απάντηση προκύπτει πανεύκολα:

Με pixel clock στα 25,175MHz εκπέμπονται 640 pixels
Με pixel clock στα 10,000MHz εκπέμπονται x   pixels

Κάνοντας τις πράξεις, προκύπτει ότι το x είναι ίσο με 254,2. Επομένως, στο χρόνο που η οθόνη περιμένει 640 pixels, το κύκλωμα γραφικών του υπολογιστή μας μπορεί να στέλνει 254. Ξέρετε κάτι; Αυτός ο αριθμός είναι τόσο κοντά στο 256, που θα ήταν αμαρτία αν δεν κάναμε τη στρογγυλοποίηση :D Αναρωτιέστε αν θα δεχτεί κάτι τέτοιο η οθόνη; Tα 8 pixels που προηγούνται από τα 640 ορατά, όπως και τα 8 που ακολουθούν, σχηματίζουν το λεγόμενο border. Η συγκεκριμένη έννοια έχει εξαλειφθεί πλέον και η εικόνα που προβάλλουν οι σύγχρονες οθόνες εκτείνεται από άκρη σε άκρη. Ωστόσο, το σήμα VGA εξακολουθεί να προβλέπει την ύπαρξη του border και παρ’ όλο που δεν χρησιμεύει για την προβολή “ωφέλιμων” δεδομένων, κανένας δεν μας εμποδίζει να το αξιοποιήσουμε για την εκπομπή δυο pixels.

Πριν προχωρήσουμε, αξίζει να κάνουμε μια μικρή ανακεφαλαίωση. Το κύκλωμα γραφικών παράγει σήμα VGA με τα τυπικά timings. Ως εκ τούτου, η εκάστοτε οθόνη “νομίζει” ότι λαμβάνει εικόνα με ανάλυση 640 x 480 pixels και με ρυθμό ανανέωσης στα 59,94Hz. Στην πραγματικότητα, μόνο το refresh rate είναι αληθινό. Η GPU παράγει εικόνα με οριζόντια ανάλυση στα 256 pixels, ενώ μια παρόμοια υποχώρηση ισχύει και για την κατακόρυφη ανάλυση. Αν το πρόγραμμά μας δημιουργούσε 480 διαφορετικές γραμμές, θα καταλήγαμε στην αλλόκοτη ανάλυση των 256 x 480 pixels. Για να αποφύγουμε αυτή την παράξενη αναλογία εικόνας και, ταυτόχρονα, να απλοποιήσουμε τη δουλειά της GPU, αποφασίσαμε να στέλνουμε κάθε γραμμή εις διπλούν. Με αυτό το κολπάκι, αν και η οθόνη εξακολουθεί να λαμβάνει 480 scanlines, η κατακόρυφη ανάλυση της εικόνας μειώνεται στο μισό. Έτσι, η κάρτα γραφικών του υπολογιστή μας προσφέρει την ανάλυση των 256 x 240 pixels. Τέλος, αν και τα πρώτα κυκλώματα VGA μπορούσαν να προβάλλουν μόνο 16 χρώματα ταυτόχρονα, το δικό μας μπορεί να προβάλλει 256! Αυτό δεν χρειάστηκε κάποια προσπάθεια από μέρους μας, αφού κάθε διεύθυνση της frame buffer αντιστοιχεί σε ένα pixel και μπορεί να αποθηκεύσει ένα ολόκληρο byte.

Commodore 64: Ο πιο επιτυχημένος υπολογιστής όλων των εποχών! Μπορεί οι πωλήσεις του να μην έχουν καμία σχέση με το θέμα μας, αλλά στην οθόνη του φαίνεται το λεγόμενο border. Πρόκειται για την περιοχή με το ανοιχτό γαλάζιο χρώμα.

Commodore 64: Ο πιο επιτυχημένος υπολογιστής όλων των εποχών! Μπορεί οι πωλήσεις του να μην έχουν καμία σχέση με το θέμα μας, αλλά στην οθόνη του φαίνεται το λεγόμενο border. Πρόκειται για την περιοχή με το ανοιχτό γαλάζιο χρώμα.

Το πρόγραμμα
Έχοντας ξεδιαλύνει όλα τα θεωρητικά ζητήματα που εμπλέκονται στη δημιουργία του σήματος VGA, μπορούμε να προχωρήσουμε στον κώδικα. Όπως θα έχετε υποψιαστεί, το πρόγραμμα είναι γραμμένο σε Assembly. Η παραγωγή του σήματος VGA απαιτεί ακρίβεια σε επίπεδο CPU cycle και κάτι τέτοιο μπορεί να επιτευχθεί μόνο στην Assembly. Πριν ξεκινήσουμε την παρουσίαση, έχετε υπόψη ότι μπορείτε να κατεβάσετε τα πάντα από εδώ. Στο πακέτο θα βρείτε τα αρχεία με τον πηγαίο κώδικα καθώς και το αρχείο ΗΕΧ, που μπορεί να γραφτεί απευθείας στον μικροελεγκτή. Παρεμπιπτόντως, σημειώστε ότι εργαστήκαμε στα Windows και χρησιμοποιήσαμε τον assembler της ATMEL (avrasm2). Αν εργάζεστε στο Linux, μπορείτε να χρησιμοποιήσετε τον συμβατό assembler ονόματι avra. Ο κώδικας του προγράμματος εκτείνεται σε αρκετά διαφορετικά αρχεία. Το καθένα περιέχει συγγενικές συναρτήσεις και ο διαχωρισμός αποσκοπεί στην ανάδειξη της γενικότερης δομής του προγράμματος. Μην ξεχνάτε ότι η γλώσσα Assembly δεν είναι καθόλου φιλική και, επιπρόσθετα, μια μόνο συνάρτηση μπορεί να εκτείνεται σε δεκάδες γραμμές. Αν δεν χωρίζαμε τον κώδικα, η μελέτη και η συντήρησή του θα ήταν πρακτικά αδύνατη.

Ο κώδικας ξεκινά στο αρχείο vga-fw.asm και από εκεί φορτώνονται όλα τα υπόλοιπα, με τη βοήθεια των δηλώσεων include. Προφανώς το αρχείο 0.macros.asm περιλαμβάνει όσα macros ορίσαμε, σε μια προσπάθεια να καταστήσουμε τον κώδικα ελαφρώς πιο ευανάγνωστο. Ο κώδικας του αρχείου 1.prep.asm φροντίζει για τη λήψη των εντολών και των αντίστοιχων παραμέτρων που στέλνει ο επεξεργαστής. Μέσα στο 2.buffer.asm βρίσκονται οι ρουτίνες που χειρίζονται τη frame buffer. Για παράδειγμα, θα βρείτε τη ρουτίνα που καθαρίζει την οθόνη, τυπώνει χαρακτήρες, ρολάρει την εικόνα, κ.λπ. Οι ρουτίνες που σχετίζονται με το χειρισμό του δρομέα έχουν τοποθετηθεί στο αρχείο 2.cursor.asm. Τέλος, στα αρχεία που το όνομά τους ξεκινά με τον αριθμό 3 περιλαμβάνονται όλες οι σταθερές: Δεδομένα που δεν μεταβάλλονται κατά την εκτέλεση του προγράμματος, όπως η γραμματοσειρά, η εικόνα/λογότυπο του υπολογιστή κ.ά. Η παραπάνω περιγραφή ενδέχεται να σας προκάλεσε σύγχυση, αφού αναφέρθηκαν τμήματα του κώδικα για τα οποία δεν έχουμε μιλήσει καθόλου. Για την ώρα μπορείτε να ξεχάσετε οτιδήποτε σας προβλημάτισε και να επικεντρώσετε στο θέμα που μας έχει απασχολήσει ως τώρα: η παραγωγή του σήματος εικόνας.

Παραγωγή γραμμών
Το πρόγραμμα καταφέρνει να μεταφέρει τα περιεχόμενα της frame buffer στην οθόνη, δημιουργώντας το ένα scanline μετά το άλλο. Όπως είδαμε νωρίτερα, οι προδιαγραφές του σήματος VGA καθορίζουν ότι τα scnalines πρέπει να εκπέμπονται με συχνότητα 31,469KHz. Αυτή η συχνότητα αντιστοιχεί σε μια περίοδο των 31,77μsec και αν λάβουμε υπόψη ότι ο μικροελεγκτής μας είναι χρονισμένος στα 20MHz, το συγκεκριμένο χρονικό διάστημα αντιστοιχεί σε περίπου 635 CPU cycles. Για να εξασφαλίσουμε τη μεγάλη ακρίβεια που απαιτεί το σήμα VGA, ο κώδικας που σχηματίζει τα scanlines τοποθετήθηκε στη ρουτίνα εξυπηρέτησης ενός interrupt (ISR). Το εν λόγω interrupt παράγεται από έναν μετρητή (timer/counter1), κάθε φορά που η τιμή του φτάνει στο 635. Τελικά, επειδή και πάλι μπορεί να σας μπερδέψαμε, κρατήστε το εξής: Όλη η δύσκολη δουλειά, δηλαδή ο σχηματισμός των διαδοχικών scnalines, πραγματοποιείται μέσα στη ρουτίνα εξυπηρέτησης ενός interrupt. Στον κώδικά μας η εν λόγω ρουτίνα έχει το (ευρηματικό) όνομα scanline και ξεκινά στη γραμμή 253 του αρχείου vga-fw.asm. Πριν προχωρήσουμε αξίζει να σημειώσουμε ότι όλες οι υπόλοιπες εργασίες της GPU (επικοινωνία με τον επεξεργαστή, εκτύπωση χαρακτήρων κ.ά.) πραγματοποιούνται κατά τη διάρκεια του vertical video blanking interval. Αυτή η περίοδος διαρκεί για 29 scanlines και οι συγκεκριμένες γραμμές περιλαμβάνουν μόνο τον παλμό HSYNC. Ως εκ τούτου, η παραγωγή τους ολοκληρώνεται πολύ γρήγορα και περισσεύει αρκετός χρόνος μέχρι το επόμενο interrupt.

Λίγο πίσω από την ακτίνα
Μη νομίζετε ότι το σήμα VGA έχει το μονοπώλιο στην αυστηρότητα ή στην πολυπλοκότητα. Τα αναλογικά τηλεοπτικά σήματα παρουσιάζουν τις ίδιες ιδιοτροπίες και, επιπρόσθετα, εκτός από εικόνα μεταφέρουν και ήχο. Αν πιστεύετε ότι η σύνθεση σήματος VGA αποτελεί την πιο επίπονη δουλειά για έναν προγραμματιστή, πρέπει να νιώθετε και τυχεροί που δεν γεννηθήκατε μερικές δεκαετίες νωρίτερα. Στα μέσα της δεκαετίας του 80, η έννοια της παιχνιδομηχανής ήταν σχεδόν ταυτισμένη με τα συστήματα της Atari. Δεν αναφερόμαστε στους ομώνυμους υπολογιστές, αλλά στις συσκευές που συνδέονταν στην τηλεόραση και είχαν σαν μοναδική αποστολή το παιχνίδι. Ένας λόγος που οδήγησε στην επιτυχία τους ήταν το πολύ χαμηλό κόστος. Ξέρετε πώς το εξασφάλισαν αυτό οι μηχανικοί της εταιρείας; Εκείνη την εποχή οι μνήμες αποτελούσαν μακράν το πιο ακριβό εξάρτημα σε ένα σύστημα. Οι παιχνιδομηχανές Atari, λοιπόν, στηρίζονταν σ’ ένα κύκλωμα γραφικών (το περίφημο TIA) που δεν διέθετε μνήμη για την αποθήκευση της εικόνας. Εν ολίγοις, τα Atari δεν διέθεταν frame buffer! Σε αυτά τα συστήματα η παραγωγή του τηλεοπτικού σήματος αποτελούσε ευθύνη του hardware, αλλά αυτό δεν διευκόλυνε καθόλου τη ζωή των προγραμματιστών. Τα προγράμματα έπρεπε να υπολογίζουν τη θέση των διαφόρων αντικειμένων και να σχεδιάζουν όλα τα γραφικά ξανά και ξανά. Μάλιστα, κάθε γραμμή της εικόνας έπρεπε να σχεδιάζεται ακριβώς τη στιγμή που θα ξεκινούσε η εκπομπή του αντίστοιχου scanline. Γι’ αυτό το λόγο, οι προγραμματιστικές τεχνικές που αναπτύχθηκαν στα Atari έμειναν στην ιστορία με το γενικό όνομα “racing the beam”. Τι έχετε να πείτε για τους προγραμματιστές που έγραφαν τα παιχνίδια της πλατφόρμας;

Η πιο διάσημη παιχνιδομηχανή του 80! Οι gamers την αγαπούσαν για το χαμηλό κόστος και τη μεγάλη ποικιλία παιχνιδιών, ενώ οι προγραμματιστές την αντιπαθούσαν εξαιτίας του πολύ περίπλοκου και δύσκολου προγραμματισμού της. Παρεμπιπτόντως, σε αυτή την παιχνιδομηχανή έχουμε παίξει Berzerk, Dig Dug, Defender, Combat και Outlaw! Εσείς;

Η πιο διάσημη παιχνιδομηχανή του 80! Οι gamers την αγαπούσαν για το χαμηλό κόστος και τη μεγάλη ποικιλία παιχνιδιών, ενώ οι προγραμματιστές την αντιπαθούσαν εξαιτίας του πολύ περίπλοκου και δύσκολου προγραμματισμού της. Παρεμπιπτόντως, σε αυτή την παιχνιδομηχανή έχουμε παίξει Berzerk, Dig Dug, Defender, Combat και Outlaw! Εσείς;

Περισσότερη ακρίβεια
Η ανάγκη να ικανοποιήσουμε τόσο αυστηρούς χρονικούς περιορισμός προκύπτει πολύ σπάνια. Ως αποτέλεσμα, δεν ασχολούμαστε ποτέ με το πόσος χρόνος μεσολαβεί από την εμφάνιση ενός interrupt έως την εκτέλεση της αντίστοιχης ρουτίνας χειρισμού. Για τη συντριπτική πλειονότητα των εφαρμογών, αυτό το χρονικό διάστημα μπορεί να θεωρηθεί μηδενικό. Στην πραγματικότητα, τα πράγματα δεν είναι τόσο απλά (κλασικά). Το εν λόγω χρονικό διάστημα δεν είναι ούτε μηδενικό, ούτε σταθερό! Όταν προκύπτει κάποιο interrupt ο μικροελεγκτής μεταβαίνει σε μια ειδική διεύθυνση της μνήμης προγράμματος, που ονομάζεται interrupt vector. Φυσικά, για κάθε interrupt υπάρχει κι ένα αντίστοιχο interrupt vector. Μπορείτε να δείτε τις σχετικές διευθύνσεις στην ενότητα “Interrupt Vectors in ATmega644” του datasheet. Εξαιτίας των παραπάνω, το πρόγραμμά μας ξεκινά κάπως έτσι:

.org 0
     rjmp mcu_init        ; Reset-Interrupt Handler
.org OC1Aaddr
     rjmp scanline        ; Timer1 CompareA-Interrupt Handler

Στη διεύθυνση που έχει αντιστοιχιστεί το reset interrupt, έχουμε τοποθετήσει μια παραπομπή προς το σημείο εκκίνησης του προγράμματός μας — τον κώδικα που αρχικοποιεί τον μικροελεγκτή. Στη διεύθυνση OC1Aaddr, που αντιστοιχεί στο interrupt που αξιοποιεί το πρόγραμμά μας, έχουμε τοποθετήσει μια παραπομπή προς την αντίστοιχη ρουτίνα εξυπηρέτησης. Πιστεύουμε ότι όλα αυτά είναι απλά και κατανοητά. Δείτε όμως μια ύπουλη λεπτομέρεια: Όταν προκύπτει κάποιο interrupt, ο μικροελεγκτής δεν διακόπτει την εκτέλεση της τρέχουσας εντολής. Περιμένει την ολοκλήρωσή της και μόνο τότε μεταβαίνει στην κατάλληλη διεύθυνση (interrupt vector). Αυτό σημαίνει ότι, αν εκείνη τη στιγμή εκτελείται μια εντολή που διαρκεί παραπάνω από ένα CPU cycle, η εκτέλεση της ρουτίνας εξυπηρέτησης θα καθυστερήσει κατά δύο ή περισσότερα CPU cycles. Τελικά, από την εμφάνιση ενός interrupt μέχρι την εκτέλεση του αντίστοιχου ISR, μεσολαβούν από 5 έως 9 CPU cycles. Ε, λοιπόν, αυτή η αβεβαιότητα συνιστά μεγάλο πρόβλημα για το πρόγραμμά μας. Αν τα διάφορα scanlines δημιουργούνται άλλοτε νωρίτερα και άλλοτε αργότερα, η συχνότητα παραγωγής τους δεν θα είναι σταθερή. Κατά συνέπεια, η οθόνη θα δυσκολεύεται να “κλειδώσει” στο σήμα και θα παρατηρούμε ένα τρεμόπαιγμα ή δεν θα βλέπουμε τίποτα.

Για καλή μας τύχη, οι εντολές που διαρκούν 3, 4 ή 5 CPU cycles είναι πολύ λίγες και στο δικό μας πρόγραμμα έχουν χρησιμοποιηθεί ελάχιστα. (Η αλήθεια είναι ότι δεν καταβάλαμε μεγάλη προσπάθεια, αφού οι εν λόγω εντολές σχετίζονται με ελέγχους και λειτουργίες που χρησιμοποιούνται σπάνια.) Αυτό σημαίνει ότι κάθε φορά που προκύπτει το interrupt για την παραγωγή μιας γραμμής, εισάγεται καθυστέρηση των 5 ή των 6 CPU cycles. Αυτή η ασάφεια εξακολουθεί να αποτελεί πρόβλημα, αλλά έχει μικρότερο εύρος και αντιμετωπίζεται πιο εύκολα. Δείτε πώς ξεκινάει η ρουτίνα που δημιουργεί τα scanlines:

scanline:
     lds isr_temp1, TCNT1L   ; (2)
     sbrc isr_temp1, 0       ; (1~2)
     rjmp vdelay_end         ; (2)
vdelay_end:

Αναρωτιέστε τι πετυχαίνουμε εδώ; Όταν ο χρόνος για την εκκίνηση της ρουτίνας φτάνει στους 5 κύκλους (CPU cycles), οι παραπάνω γραμμές εισάγουν μια καθυστέρηση που διαρκεί επίσης 5 κύκλους. Όταν όμως το αρχικό χρονικό διάστημα φτάνει στους 6 κύκλους, ο κώδικας εισάγει μια καθυστέρηση των 4 κύκλων. Έτσι, η παραγωγή των scanlines ξεκινάει πάντα με την ίδια καθυστέρηση κι επαναλαμβάνεται με απόλυτα σταθερή συχνότητα! Ας δούμε πώς το πετύχαμε αυτό. Όπως είπαμε, το interrupt για τη δημιουργία των γραμμών προκύπτει κάθε φορά που ο timer/counter1 φτάνει στην τιμή 635. Ωστόσο, την ίδια στιγμή ο μετρητής μηδενίζεται και αρχίζει να αυξάνεται από το επόμενο CPU cycle, έως ότου φτάσει και πάλι στο 635. Αν η καθυστέρηση που θέλουμε να αντισταθμίσουμε φτάσει στους 5 κύκλους, όταν ξεκινήσει η ρουτίνα scanline ο μετρητής θα έχει την τιμή 5. Αντίστοιχα, αν η καθυστέρηση ανέλθει στους 6 κύκλους, ο μετρητής θα έχει την τιμή 6. Όπως βλέπετε, στην πρώτη περίπτωση η τιμή του μετρητή είναι περιττή και στη δεύτερη είναι άρτια. Θυμηθείτε το αυτό. Η πρώτη εντολή του παραπάνω αποσπάσματος λαμβάνει το “λιγότερο σημαντικό” byte (Least Significant Byte) της τιμής του μετρητή και το αποθηκεύει στον καταχωριστή isr_temp1. Στη συνέχεια ελέγχουμε αν το πρώτο bit του συγκεκριμένου καταχωριστή είναι 0 ή 1. Στην πρώτη περίπτωση (άρτια τιμή) η εντολή ελέγχου διαρκεί δύο κύκλους και η επόμενη εντολή παρακάμπτεται. Στη δεύτερη περίπτωση (περιττή τιμή) η εντολή ελέγχου διαρκεί έναν κύκλο και η επόμενη εντολή, που διαρκεί για δύο κύκλους, εκτελείται κανονικά. Αν κάνετε τις προσθέσεις, θα διαπιστώσετε ότι σε κάθε περίπτωση το άθροισμα των καθυστερήσεων ισούται με 10 κύκλους. Απλό δεν ήταν;

Πού είμαι; Τι κάνω;
Ελπίζουμε να μη σας τρόμαξε η προηγούμενη παράγραφος. Πάντως το απόσπασμα που εξετάσαμε αποτελεί το πιο σκοτεινό τμήμα ολόκληρου του προγράμματος. Ο υπόλοιπος κώδικας είναι συγκριτικά απλούστερος. Η ρουτίνα scanline δημιουργεί τον παλμό οριζόντιου συγχρονισμού και ταυτόχρονα πραγματοποιεί μερικές άσχετες εργασίες. Σε αυτές θα αναφερθούμε στο επόμενο άρθρο της σειράς, όταν θα εξετάσουμε τις υπόλοιπες λειτουργίες του προγράμματος. Μετά από τον παλμό HSYNC ακολουθεί το back porch, ενώ στη συνέχεια πραγματοποιείται η ανάγνωση μιας γραμμής από τη frame buffer. Τα σχετικά δεδομένα αποτελούν το “ορατό” τμήμα του scanline και γι’ αυτό κατευθύνονται προς τον DAC κι από ‘κεί προς την οθόνη. Η ρουτίνα scanline δεν κάνει τίποτα περίπλοκο, αλλά είναι βέβαιο ότι η περιγραφή μάς έχει γεννήσει νέα ερωτήματα: Πότε δημιουργείται ο κατακόρυφος παλμός συγχρονισμού (VSYNC); Ποιες γραμμές είναι ενεργές και ποιες ανήκουν στο vertical video blanking interval; Σε ποια διεύθυνση της frame buffer βρίσκονται τα δεδομένα της εκάστοτε γραμμής;

Ο κώδικας θα μπορούσε να διατηρεί έναν μετρητή γραμμών και, σύμφωνα με την τιμή του, να υπολογίζει την απάντηση σε όλα τα παραπάνω ερωτήματα. Ωστόσο ένας τέτοιος μηχανισμός θα ήταν αρκετά χρονοβόρος και δεν θα άφηνε κανένα περιθώριο για τις “άσχετες” εργασίες που αναφέραμε προηγουμένως. Αυτό μας υποχρέωσε να υιοθετήσουμε μια λύση που ήταν λίγο πιο κουραστική για εμάς, αλλά πολύ πιο ξεκούραστη για το πρόγραμμα. Συγκροτήσαμε έναν πίνακα με ένα ζεύγος τιμών για κάθε μία από τις 525 γραμμές του frame. Η πρώτη τιμή του ζευγαριού (το πρώτο byte) περιλαμβάνει μια σημαία (flag) που δείχνει αν πρέπει να ενεργοποιηθεί ο παλμός κατακόρυφου συγχρονισμού ή όχι. Το ίδιο byte περιλαμβάνει και ένα δεύτερο flag, που δείχνει αν η συγκεκριμένη γραμμή ανήκει στην περιοχή του vertical video blanking. Η δεύτερη τιμή σε κάθε ζευγάρι του πίνακα χρησιμεύει για τις ορατές γραμμές. Αποτελεί το “περισσότερο σημαντικό” byte (Most Significant Byte) της διεύθυνσης μέσα στη frame buffer, όπου βρίσκονται οι τιμές των pixels της τρέχουσας γραμμής. Όπως αντιλαμβάνεστε, η συγκρότηση αυτού του πίνακα ήθελε μεγάλη προσοχή και υπομονή. Όταν ολοκληρώθηκε όμως δεν χρειάστηκε να επαναληφθεί ποτέ ξανά, ενώ το πρόγραμμα είχε αποκτήσει έναν γρήγορο τρόπο για να γνωρίζει σε ποια γραμμή βρίσκεται και τι πρέπει να κάνει. Μπορείτε να δείτε τον πίνακα στο αρχείο 3.lines.asm. Η ρουτίνα scanline χρησιμοποιεί έναν δείκτη για να διαβάζει κάθε φορά ένα ζεύγος τιμών από τον πίνακα. Ο συγκεκριμένος δείκτης αυξάνεται αυτόματα μετά από κάθε ανάγνωση κι έτσι είναι πάντοτε έτοιμος προς χρήση. Προσέξτε τώρα μια ενδιαφέρουσα λεπτομέρεια: Στο τέλος του πίνακα έχουμε προσθέσει ένα byte με την τιμή 255, η οποία δεν εμφανίζεται σε κανένα άλλο στοιχείο του πίνακα. Ο κώδικας τσεκάρει το byte που λαμβάνει κάθε φορά και αν διαπιστώσει ότι έχει την τιμή 255 αντιλαμβάνεται ότι έχει φτάσει στο τέλος του πίνακα κι επαναφέρει τον δείκτη στην αρχική του τιμή. Με αυτόν τον τρόπο, ο δείκτης βρίσκεται πάντοτε μέσα σε αποδεκτά όρια και η ρουτίνα scanline δεν χρειάζεται να πραγματοποιεί χρονοβόρους ελέγχους, ούτε να θυμάται τον αριθμό της τρέχουσας γραμμής. Ωραίο κολπάκι, δεν μπορείτε να πείτε ;)

Ορατά pixels
Μέχρι στιγμής έχουμε αναφέρει επανειλημμένα ότι τα δεδομένα της εικόνας αποθηκεύονται στη frame buffer, αλλά δεν έχουμε πει τίποτα για την οργάνωσή τους. Κατ’ αρχάς, σημειώστε ότι εφόσον η εικόνα έχει ανάλυση 256 x 240 και χρησιμοποιούμε ένα byte για κάθε pixel, η αποθήκευση μιας ολόκληρης εικόνας απαιτεί 61440 bytes. Ας ξεκινήσουμε τώρα από το τσιπάκι μνήμης και τον τρόπο που το χειριζόμαστε. Επειδή η χωρητικότητά του είναι 128KB, διαθέτει 17 εισόδους διεύθυνσης (2^17 = 128Κ). Ωστόσο, αν ρίξετε μια ματιά στο κύκλωμα θα διαπιστώσετε ότι το address bus που συνδέει τη μνήμη με τον μικροελεγκτή έχει εύρος 16 bits. Η δέκατη έβδομη είσοδος διεύθυνσης (το περισσότερο σημαντικό ψηφίο) συνδέεται σε έναν άσχετο ακροδέκτη του μικροελεγκτή. Από αυτόν, ο μικροελεγκτής μπορεί να επιλέγει το αν θα εργάζεται στα “άνω” 64KB ή στα “κάτω”. Εν ολίγοις, μέσα στο τσιπάκι μνήμης υπάρχει χώρος για τη φιλοξενία δύο frame buffers, ενώ υπάρχει και η υποδομή για τον χειρισμό τους. Πάντως πρέπει να ομολογήσουμε ότι ο κώδικας που έχουμε γράψει μέχρι στιγμής αξιοποιεί μόνο τη μία buffer.

Σε αυτή την περιοχή του κυκλώματος φαίνεται το address bus, που συνδέει τη μνήμη με τον μικροελεγκτή. Παρατηρήστε ότι το 'περισσότερο σημαντικό' bit των διευθύνσεων που δέχεται η μνήμη (είσοδος A16) συνδέεται στον ακροδέκτη PB0 του μικροελεγκτή και δεν ανήκει στο address bus. Από αυτό το bit, ο μικροελεγκτής μπορεί να καθορίζει αν το address bus θα παραπέμπει σε κάποια θέση των 'άνω' 64ΚΒ ή των 'κάτω'.

Σε αυτή την περιοχή του κυκλώματος φαίνεται το address bus, που συνδέει τη μνήμη με τον μικροελεγκτή. Παρατηρήστε ότι το “περισσότερο σημαντικό” bit των διευθύνσεων που δέχεται η μνήμη (είσοδος A16) συνδέεται στον ακροδέκτη PB0 του μικροελεγκτή και δεν ανήκει στο address bus. Από αυτό το bit, ο μικροελεγκτής μπορεί να καθορίζει αν το address bus θα παραπέμπει σε κάποια θέση των “άνω” 64ΚΒ ή των “κάτω”.

Ας εστιάσουμε και στο εσωτερικό των buffers. Το μέγεθος της κάθε μίας είναι 64KB, ενώ μια ολόκληρη εικόνα απαιτεί μόλις 61440 bytes. Αυτά τα δεδομένα φροντίζουμε να αποθηκεύονται με τη σειρά που εμφανίζονται και στην οθόνη. Έτσι, τα 256 bytes που περιγράφουν τα pixels της πρώτης γραμμής της εικόνας, αποθηκεύονται στις πρώτες 256 θέσεις της μνήμης και πάει λέγοντας. Με αυτόν τον τρόπο κι επειδή κάθε γραμμή περιγράφεται με ακριβώς 256 bytes, προκύπτει ένα πολύ βολικό σύστημα διευθυνσιοδότησης: Τo “περισσότερο σημαντικό” byte του address bus αναφέρεται στη γραμμή της εικόνας, ενώ το “λιγότερο σημαντικό” byte αναφέρεται στο pixel της επιλεγμένης γραμμής. Για παράδειγμα, αν θέλουμε να διαβάσουμε την τιμή του 23ου pixel της 100ης γραμμής της εικόνας, αρκεί να τοποθετήσουμε στο high-byte του address bus την τιμή 100 και στο low-byte την τιμή 23. Αυτό το σύστημα είναι απλό και κατανοητό από τους ανθρώπους, ενώ επιτρέπει και την ταχύτατη πρόσβαση στη frame buffer.

Θυμόσαστε τη συζήτηση για το μέγιστο pixel clock που μπορεί να πετύχει το κύκλωμά μας; Για να “πιάσουμε” την ταχύτητα των 10MHz, οι διευθύνσεις μνήμης πρέπει να παράγονται με μία εντολή του ενός CPU cycle! Ε, λοιπόν, με τον τρόπο που έχουμε οργανώσει τα δεδομένα μέσα στο frame buffer, κάτι τέτοιο είναι απόλυτα εφικτό. Λίγο πριν ξεκινήσει το ορατό τμήμα του εκάστοτε scanline (όσο βρισκόμαστε στο back porch), φορτώνουμε στο high-byte του address bus τον αριθμό της αντίστοιχης γραμμής. Προφανώς, εφόσον “εκπέμπουμε” ένα συγκεκριμένο scanline, αυτό το μέγεθος δεν πρόκειται να αλλάξει και μπορούμε να το ξεχάσουμε. Επιπρόσθετα, μέσα στο χρόνο του back porch θέτουμε την τιμή ενός προσωρινού καταχωριστή σε 255. Αυτός ο καταχωριστής χρησιμοποιείται σαν το low-byte της διεύθυνσης. Όταν ξεκινήσει το ορατό τμήμα της γραμμής, θέλουμε να πιάσουμε τη μέγιστη ταχύτητα υπολογισμού και εξόδου των διευθύνσεων. Έτσι, το πρόγραμμά μας επαναλαμβάνει μόνο τις εξής δύο ενέργειες: Αυξάνει τον καταχωριστή με το low-byte κατά μία μονάδα και μεταφέρει την τιμή του στους ακροδέκτες. Καθεμία από τις συγκεκριμένες εντολές διαρκεί ακριβώς ένα CPU cycle και αυτό το ζεύγος επαναλαμβάνεται 256 φορές. Την πρώτη φορά που επαναλαμβάνεται το ζεύγος ο καταχωριστής με το low-byte μηδενίζεται (η πρόσθεση μιας μονάδας σε ένα byte που έχει την τιμή 255 έχει ως συνέπεια την υπερχείλιση του byte και την “επιστροφή” του στο μηδέν), ενώ την τελευταία φορά φτάνει στην τιμή 255. Οι 256 επαναλήψεις των δύο εντολών βρίσκονται αραδιασμένες σε ένα macro με το όνομα get_line_data, στο αρχείο 0.macros.asm. Με αυτή την άκομψη αλλά ταχύτατη μέθοδο, πετυχαίνουμε το στόχο μας κι ανεβάζουμε το pixel clock στα 10MHz.

Κάπου εδώ θα σας αφήσουμε να μελετήσετε τον κώδικα της ρουτίνας scanline. Δεδομένου ότι είναι γραμμένος σε Assembly, μην προσπαθείτε να ερμηνεύσετε κάθε γραμμή ξεχωριστά. Ακόμα κι ένας απλός έλεγχος που σε άλλες γλώσσες θα ολοκληρωνόταν σε μια γραμμή, στην Assembly ενδέχεται να εκτείνεται σε τρεις ή σε τέσσερις. Είναι προτιμότερο να σταθείτε στη γενική λειτουργία του κώδικα και να μη μπλέξετε με τις ιδιαιτερότητες της γλώσσας. Εξάλλου, η εκμάθηση της Assembly δεν θα μπορούσε να ξεκινήσει μ’ ένα τόσο περίπλοκο πρόγραμμα. Στο επόμενο άρθρο της σειράς θα εξετάσουμε τα υπόλοιπα τμήματα του κώδικα και το παζλ θα συμπληρωθεί: θα δείτε πώς φτάσαμε από την παραγωγή των scanlines σε μια αργόστροφη αλλά λειτουργική GPU!

Leave a Reply

You must be logged in to post a comment.

Σύνδεση

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