Άρθρο 4: Timekeeping

Διαθέσιμα αρχεία:

Το άρθρο δημοσιεύθικε στο linux inside τέυχος 4 (Σεπτεμβρίου-Οκτοβρίου 2011).


Εισαγωγή

Καλωσήρθατε σε άλλο ένα άρθρο της σειράς προγραμματισμού πυρήνων λειτουργικών συστημάτων από το μηδέν.

Στα προηγούμενα επεισόδια ξεκινήσαμε από έναν άδειο editor buffer και επιτεθήκαμε ένα προς ένα σε πολλά από τα βασικά στοιχεία που πρέπει να φτιάξουμε σε έναν πυρήνα. Είδαμε την διαδικασία εκκίνησης του υπολογιστή και πώς μπορούμε να τον πείσουμε να φορτώσει το πρόγραμμά μας στην μνήμη και να μας περάσει τον έλεγχο. Γράψαμε έναν απλό VGA driver για να μπορούμε να γράψουμε κείμενο στην οθόνη με την δική μας printf. Βουτήξαμε στην παράξενη αρχιτεκτονική της intel και είδαμε ποια είναι τα απαραίτητα segments που πρέπει να χρησιμοποιήσουμε και πώς να τα φέρουμε στα μέτρα μας δημιουργώντας ένα ενιαίο γραμμικό διάστημα διευθύνσεων όπως το θέλουμε. Μάθαμε πως χειριζόμαστε interrupts από τον ίδιο τον επεξεργαστή, αλλά και από τις συσκευές του συστήματος. Είδαμε πώς δουλεύει το paging hardware του επεξεργαστή για την μετάφραση εικονικών διευθύνσεων σε φυσικές διευθύνσεις και πώς κάνουμε map και unmap pages με την βοήθεια του page table. Τέλος υλοποιήσαμε διαχειριστές μνήμης που δεσμεύουν και αποδεσμεύουν physical και virtual pages για τις ανάγκες του συστήματος μας.

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

Σε αυτό το άρθρο λοιπών κατ' αρχάς θα κλείσουμε το κεφάλαιο της διαχείρισης μνήμης, περιγράφοντας την υλοποίηση των malloc και free στην klibc, που πατάνε πάνω στον page allocator του προηγούμενου τεύχους για να μας δώσουν fine-grained διαχείριση της μνήμης του πυρήνα. Μετά θα δούμε πώς χρησιμοποιούμε τον timer του συστήματος για να λαμβάνουμε interrupts σε τακτά διαστήματα, και να κρατάμε τον χρόνο με υψηλή ανάλυση. Και τέλος θα δούμε πως διαχειριζόμαστε την ημερομηνία και ώρα, και πώς διαβάζουμε από το real time clock του υπολογιστή.

Όπως πάντα ο συνοδευτικός κώδικας του άρθρου βρίσκεται στο DVD του περιοδικού, καθώς και στο web site της σειράς: http://nuclear.sdf-eu.org/articles/kerneldev. Στο ίδιο site θα βρείτε και τα προηγούμενα άρθρα της σειράς διαθέσιμα υπό τους όρους της άδειας Creative Commons Attribution Share-Alike.

Kernel malloc

Στο προηγούμενο άρθρο υλοποιήσαμε την συνάρτηση pgalloc, που βρίσκει ένα ελεύθερο συνεχές κομμάτι από όσα virtual pages ζητηθούν, τα οποία μας παρέχει αφού πρώτα τα κάνει map σε αντίστοιχο αριθμό από physical pages. Για την δέσμευση των virtual pages είδαμε ότι μπορούμε να κρατάμε μια λίστα από διαστήματα συνεχών ελεύθερων pages, την οποία διασχίζουμε ψάχνοντας το πρώτο τέτοιο διάστημα με αρκετά pages για να καλύψει τις ανάγκες μας (first fit). Την ίδια ακριβώς στρατηγική μπορούμε να ακολουθήσουμε και για την malloc (klibc/malloc.c), με μοναδική διαφορά ότι αντί να χειριζόμαστε διαστήματα από pages, χειριζόμαστε διαστήματα από bytes σε pages που έχουμε ήδη δεσμεύσει καλώντας την pgalloc.

Άλλη μια διαφορά προκύπτει από το ότι δεν θέλουμε να χρειάζεται να περάσουμε σαν παράμετρο της free το μέγεθος της μνήμης που ελευθερώνουμε, οπότε ο allocator πρέπει να ξέρει κάπως κάθε κομμάτι μνήμης που μοίρασε τι μέγεθος είχε. Η απλούστερη μέθοδος για να έχουμε αυτή την πληροφορία διαθέσιμη είναι να δεσμεύουμε κάθε φορά λίγο μεγαλύτερο κομμάτι από το ζητούμενο, έτσι ώστε να γράφουμε αμέσως πριν την αρχή του σε γνωστό αρνητικό offset το μέγεθος που δεσμεύθηκε. Το struct alloc_desc παίζει το ρόλο αυτού του header, και περιέχει εκτός από το μέγεθος του allocation, και μια γνωστή τιμή η οποία ελέγχεται από την free. Αν δεν βρεθεί αυτή η τιμή τότε σίγουρα κάποιο σοβαρό προγραμματιστικό λάθος έχει συμβεί, είτε προσπαθήσαμε να ελευθερώσουμε από κάποιο pointer που δεν πήραμε από την malloc, ή καταλάθος γράψαμε πάνω από τον allocator descriptor. Σε αυτή την περίπτωση ο kernel πανικοβάλλεται ώστε να μην περάσει απαρατήρητο το bug.

#define MAGIC    0xbaadbeef

struct alloc_desc {
    size_t size;
    uint32_t magic;
};

Η free απλώς προσθέτει το range που ελευθερώνουμε στην free_list και καλεί την coalesce που ελέγχει αν το διάστημα αυτό γειτονεύει με κάποιο άλλο ελεύθερο διάστημα ώστε να μεγαλώσει εκείνο αντί να προσθέσει άλλο ένα node. Αυτό είναι σημαντικό γιατί αλλιώς θα κατέληγε η free_list να περιέχει ένα μεγάλο αριθμό μικρών γειτονικών διαστημάτων τα οποία πρακτικά δε θα μπορούσαν να ξαναχρησιμοποιηθούν παρά μόνο για πολύ μικρά allocations. Το ίδιο φυσικά είχαμε κάνει και στο προηγούμενο άρθρο για την pgfree.

Σε αυτή τη φάση η υλοποίηση της free δεν αποδεσμεύει ποτέ pages, προτιμώντας να τα κρατήσει στην free_list για μελλοντικά allocations. Αν δούμε ότι αυτό δημιουργεί προβλήματα εξάντλησης μνήμης στο μέλλον μπορούμε να αλλάξουμε αυτή τη στρατηγική και να ελευθερώνουμε παραδείγματος χάριν αν ξεμένουν στην free_list πάνω από τα 2/3 των pages που δεσμεύσαμε.

Timers

Μια πολύ σημαντική λειτουργία ενός πυρήνα είναι το timekeeping. Κατ' αρχάς η χρονομέτρηση χρησιμοποιείται από πολλά user programs και πρέπει να παρέχει κάπως τις σχετικές υπηρεσίες ο πυρήνας. Αλλά ακόμα πιο σημαντική είναι η χρήση τον timers σε ένα time-sharing σύστημα, γιατί πρέπει να μπορεί ο πυρήνας να διακόψει την εκτέλεση του ενεργού process ανά τακτά διαστήματα για να δώσει χρόνο σε κάποιο άλλο, έτσι ώστε να δημιουργείται η ψευδαίσθηση ότι εκτελούνται ταυτόχρονα.

Όλοι οι υπολογιστές διαθέτουν κάποιον hardware timer που σηκώνει ένα interrupt με συγκεκριμένο ρυθμό, το οποίο μπορούμε να χρησιμοποιήσουμε για να κρατάμε χρόνο στον πυρήνα. Το IBM PC είχε για αυτή την δουλειά ένα chip της intel, τον 8253 Programmable Interval Timer (PIT), το οποίο πλέων υλοποιείται σαν μέρος του chipset της motherboard και όχι σαν ξεχωριστό εξάρτημα.

Το intel 8253 παίρνει σαν είσοδο ένα clock signal που προέρχεται από κάποιον κρύσταλλο, και σε κάθε παλμό μειώνει έναν 16bit binary counter του οποίου την αρχική τιμή μπορούμε να θέσουμε προγραμματιστικά. Όταν ο counter φτάσει στο μηδέν, ενεργοποιείται το output, το οποίο στο PC είναι συνδεδεμένο με το IRQ 0 pin του 8259 PIC (Programmable Interrupt Controller), ο οποίος με τη σειρά του σηκώνει interrupt στον επεξεργαστή και διακόπτεται η κανονική εκτέλεση για να πάρει τον έλεγχο ο interrupt handler. Στο mode 3 (square wave generator) που θα χρησιμοποιήσουμε τον timer, όταν μηδενιστεί ο counter, ξεκινάει πάλι από την αρχική τιμή που είχαμε ορίσει. Υπάρχουν και άλλα modes λειτουργίας όπως "one-shot", και "strobe", τα οποία όμως δεν μας είναι εξίσου χρήσιμα για τις ανάγκες του πυρήνα και δεν θα αναφερθούμε σε αυτά.

Ακριβέστερα, το 8253 έχει όχι έναν, αλλά τρεις counters, από τους οποίους μόνο ο counter 0 είναι συνδεδεμένος με κάποιο IRQ line, και μπορεί να χρησιμοποιηθεί για να κρατάμε χρόνο στον πυρήνα. Ο counter 1 ιστορικά οδηγούσε το refresh της DRAM και δεν μπορεί να χρησιμοποιηθεί, ενώ ο counter 2 είναι συνδεδεμένος με το PC speaker, ώστε να παράγει ηχητικούς τόνους σε όποια συχνότητα θέλουμε.

Ουσιαστικά θέτοντας την αρχική τιμή του counter, πετυχαίνουμε διαίρεση της συχνότητας του αρχικού clock signal του κρυστάλλου, ο οποίος πάλλεται στα 1.193182 MHz. Για παράδειγμα αν θέσουμε στον counter αρχική τιμή 2, τότε θα χρειαστούν δύο παλμοί του clock για να φτάσει στο μηδέν και να σηκωθεί το interrupt, άρα τα interrupts συμβαίνουν με την μισή συχνότητα (596.591 KHz). Η μέγιστη τιμή που μπορούμε να θέσουμε στον counter, όντας 16bit, είναι 65535, το οποίο μας δίνει την ελάχιστη δυνατή συχνότητα παραγωγής interrupts: 1193182 / 65535 = 18.20679 Hz περίπου.

Η συνάρτηση init_timer (timer.c) καλείται για να αρχικοποιήσει τον counter 0 του PIT, βάζοντας τον στο mode λειτουργίας που θέλουμε και θέτοντας την αρχική τιμή που χρειάζεται ώστε να πετύχουμε το επιθυμητό frequency.

/* frequency of the timer ticks in hertz */
#define TICK_FREQ_HZ   250
/* frequency of the input clock signal */
#define OSC_FREQ_HZ    1193182
/* macro to divide and round to the nearest int */
#define DIV_ROUND(a, b) \
    ((a) / (b) + ((a) % (b)) / ((b) / 2))
#define PORT_DATA0       0x40
#define PORT_CMD         0x43
#define CMD_ACCESS_BOTH  (3 << 4)
#define CMD_OP_SQWAVE    (3 << 1)

void init_timer(void)
{
    /* calculate the reload count: round(osc / freq) */
    int reload = DIV_ROUND(OSC_FREQ_HZ, TICK_FREQ_HZ);

    /* set the mode to square wave for counter 0
     * low & high reload count bytes follow */
    outb(CMD_ACCESS_BOTH | CMD_OP_SQWAVE, PORT_CMD);
    outb(reload & 0xff, PORT_DATA0);
    outb((reload >> 8) & 0xff, PORT_DATA0);

    interrupt(IRQ_TO_INTR(0), intr_handler);
}

static void intr_handler()
{
    nticks++;
}

Η init_timer κατ' αρχάς υπολογίζει την αρχική τιμή που πρέπει να θέσει διαιρώντας την συχνότητα του clock με την επιθυμητή συχνότητα των timer ticks. Το πρώτο outb θέτει τον counter 0 σε square wave mode και ορίζει ότι θα ακολουθήσουν δύο bytes στο data port με την αρχική τιμή για τον counter, τα οποία στέλνονται αμέσως μετά. Τέλος η κλήση στην interrupt (intr.c) ορίζει την συνάρτηση intr_handler σαν χειριστή των interrupts που προέρχονται από IRQ 0. Η intr_handler απλώς αυξάνει κάθε φορά την μεταβλητή nticks κατά ένα, ώστε να ξέρουμε πόσα ticks (1/250 του δευτερολέπτου) πέρασαν από την εκκίνηση του kernel.

Πραγματικός χρόνος

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

Υπάρχουν πολλοί τρόποι για να κρατάει ο πυρήνας τον πραγματικό χρόνο, και κάθε σύστημα το κάνει διαφορετικά. Εμείς θα ακολουθήσουμε τον τρόπο του UNIX, το οποίο κρατάει τον χρόνο σε δευτερόλεπτα από το UNIX epoch, δηλαδή από την 1/1/1970. Κατά το initialization θα διαβάσουμε την ημερομηνία και ώρα από το ρολόι του υπολογιστή, και θα το μετατρέψουμε σε δευτερόλεπτα από το epoch. Έτσι όταν χρειαστεί να υπολογίσουμε τον χρόνο αρκεί να προσθέσουμε τα δευτερόλεπτα που πέρασαν από την εκκίνηση (nticks / TICK_FREQ_HZ), στον αρχικό χρόνο.

Όλοι οι υπολογιστές έχουν κάποιου είδους ρολόι πραγματικού χρόνου (RTC) που δουλεύει με μπαταρία ακόμα και αν ο υπολογιστής είναι σβηστός και αποσυνδεμένος από την παροχή ρεύματος. Το RTC στον IBM PC κρατάει την ημερομηνία και ώρα σε ένα chip στατικής μνήμης συνεχώς τροφοδοτούμενο από την μπαταρία, το οποίο ιστορικά λέγεται CMOS RAM.

Η ημερομηνία και ώρα είναι χωρισμένη σε μια σειρά από registers που διαβάζουμε χρησιμοποιώντας τα κλασικά I/O instructions του x86. Πρώτα γράφουμε στο control port (0x70) το νούμερο του register που μας ενδιαφέρει και μετά διαβάζουμε από το data port (0x71) την τιμή του register που διαλέξαμε. Η συνάρτηση read_reg (rtc.c) κάνει αυτή τη δουλειά.

static int read_reg(int reg)
{
    unsigned char val;
    outb(reg, PORT_CTL);
    iodelay();
    inb(val, PORT_DATA);
    iodelay();
    return val;
}

Τα iodelay (asmops.h) είναι απλά για να δώσουν αρκετό χρόνο. στο CMOS να πραγματοποιήσει την αλλαγή register πριν διαβάσουμε την τιμή. Τα νούμερα των registers που περνάμε στην read_reg είναι δηλωμένα στο rtc.c, και για παράδειγμα ο register 9 περιέχει το έτος, ο register 8 τον μήνα, κλπ.

Φυσικά τίποτα στην ταλαιπωρημένη αρχιτεκτονική του PC δεν είναι τόσο απλό. Κατ' αρχάς η ώρα μπορεί να είναι είτε σε 24ωρη μορφή, ή σε 12ωρη με το πιο σημαντικό bit να καθορίζει αν πρόκειται για προ η μετά μεσημβρίας. Επίσης ο χρόνος μπορεί να είναι είτε σε πλήρη μορφή (2011), ή σε δυο ψηφία μόνο (11) με πιο σύνηθες το δεύτερο. Τέλος οι τιμές που διαβάζουμε από όλους τους RTC registers μπορεί να είναι είτε κανονικοί δυαδικοί ακέραιοι, ή σε μορφή BCD (binary coded decimal), όπου κάθε nibble (4bit κομμάτι) χρησιμοποιείται σαν δεκαδικό ψηφίο. Την μορφή της ώρας και το αν οι τιμές είναι BCD ή binary, τα μαθαίνουμε από τα bits 1 και 2 του status B register. Η συνάρτηση read_rtc (rtc.c) χειρίζεται όλες της παραπάνω περιπτώσεις, διαβάζει όλους τους σχετικούς RTC CMOS registers και γεμίζει ένα standard "struct tm", το οποίο δηλώσαμε στο klibc/time.h.

#define BCD_TO_BIN(x) \
        ((((x) >> 4) & 0xf) * 10 + ((x) & 0xf))

static void read_rtc(struct tm *tm)
{
    int statb, pm;
    /* wait for any clock updates to finish */
    while(read_reg(REG_STATA) & STATA_BUSY);

    tm->tm_sec = read_reg(REG_SEC);
    tm->tm_min = read_reg(REG_MIN);
    tm->tm_hour = read_reg(REG_HOUR);
    tm->tm_mday = read_reg(REG_DAY);
    tm->tm_mon = read_reg(REG_MONTH);
    tm->tm_year = read_reg(REG_YEAR);

    /* in 12hour mode, bit 7 means post-meridiem */
    pm = tm->tm_hour & HOUR_PM_BIT;
    tm->tm_hour &= ~HOUR_PM_BIT;

    /* convert to binary if needed */
    statb = read_reg(REG_STATB);
    if(!(statb & STATB_BIN)) {
        tm->tm_sec = BCD_TO_BIN(tm->tm_sec);
        tm->tm_min = BCD_TO_BIN(tm->tm_min);
        tm->tm_hour = BCD_TO_BIN(tm->tm_hour);
        tm->tm_mday = BCD_TO_BIN(tm->tm_mday);
        tm->tm_mon = BCD_TO_BIN(tm->tm_mon);
        tm->tm_year = BCD_TO_BIN(tm->tm_year);
    }
    /* make the year an offset from 1900 */
    if(tm->tm_year < 100) {
        tm->tm_year += 100;
    } else {
        tm->tm_year -= 1900;
    }
    /* if tm_hour is in 12h mode, convert to 24h */
    if(!(statb & STATB_24HR)) {
        if(tm->tm_hour == 12) {
            tm->tm_hour = 0;
        }
        if(pm) {
            tm->tm_hour += 12;
        }
    }
    tm->tm_mon -= 1;  /* make months start from 0 */
}

Τέλος η init_rtc (rtc.c) που καλείται από την main του πυρήνα, αυτό που κάνει είναι να καλέσει καταρχάς την read_rtc που μόλις είδαμε, και μετά δίνει το tm structure που γέμισε η read_rtc στην mktime για να το μετατρέψει σε δευτερόλεπτα από το epoch, τα οποία και κρατάει στην global μεταβλητή start_time που είναι προσβάσιμη από όλο τον υπόλοιπο kernel.

Standard C συναρτήσεις χρόνου

Φυσικά οι συναρτήσεις mktime και gmtime που μετατρέπουν από struct tm σε δευτερόλεπτα από το epoch και το αντίστροφο, όπως και όλες οι συναρτήσεις της standard C library, δεν υπάρχουν αν δεν τις φτιάξουμε μόνοι μας στην klibc.

Δυστυχώς το προφανές που θα ήταν να πολλαπλασιάσουμε τα χρόνια με τον αριθμό δευτερολέπτων ανά χρόνο, μετά τους μήνες με τον αριθμό δευτερολέπτων ανά μήνα κλπ δεν λειτουργεί, γιατί πρέπει να συνυπολογίσουμε τα δίσεκτα έτη (leap years), στα οποία ο φεβρουάριος έχει 29 αντί για 28 ημέρες. Δίσεκτα κατά το γρηγοριανό ημερολόγιο είναι όλα τα χρόνια που διαιρούνται ακριβώς με το 4 ή με το 400, εξαιρουμένων αυτών που διαιρούνται ακριβώς με το 100. Η συνάρτηση is_leap_year στο klibc/time.c μας απαντάει στο εάν κάποιο έτος είναι δίσεκτο ή όχι. Η day_of_year στο ίδιο αρχείο, παίρνει σαν παράμετρο το έτος, μήνα και ημέρα του μήνα, και μας επιστρέφει πια ημέρα του χρόνου είναι αυτή λαμβάνοντας υπόψιν το αν είναι δίσεκτο ή όχι το έτος για το οποίο μιλάμε.

Χρησιμοποιώντας τα παραπάνω, η mktime μπορεί να υλοποιηθεί ως εξής:

#define MINSEC      60
#define HOURSEC     (60 * MINSEC)
#define DAYSEC      (24 * HOURSEC)
#define YEARDAYS(x) (is_leap_year(x) ? 366 : 365)
#define EPOCH_WDAY  4  /* 1-1-1970 was thursday */

time_t mktime(struct tm *tm)
{
    int i, num_years = tm->tm_year - 70;
    int year = 1970;
    int days = day_of_year(tm->tm_year + 1900,
            tm->tm_mon, tm->tm_mday - 1);
    tm->tm_yday = days;  /* fix yearday */

    for(i=0; itm_wday = (days + EPOCH_WDAY) % 7;

    return days * DAYSEC + tm->tm_hour * HOURSEC +
        tm->tm_min * MINSEC + tm->tm_sec;
}

Η gmtime κάνει το αντίστροφο, δηλαδή παίρνει δευτερόλεπτα από το epoch και μας γεμίζει ένα struct tm. Λόγο χώρου δεν θα την συμπεριλάβουμε εδώ, δείτε τον κώδικα στο klibc/time.c. Συνοπτικά αυτό που κάνει είναι να ξεκινάει υπολογίζοντας πόσες μέρες αντιστοιχούν στα δευτερόλεπτα που τις δίνουμε (t / DAYSEC). Μετά ξεκινώντας από το 1970 μειώνει από αυτές τις μέρες που αντιστοιχούν σε κάθε έτος, μέχρι να φτάσει να έχει λιγότερες από 365 ή 366 μέρες. Τότε βρίσκει τον μήνα με παρόμοιο τρόπο και οι υπόλοιπες μέρες που μένουν είναι η μέρα του μήνα. Από τα δευτερόλεπτα που δεν φτάνουν να συμπληρώσουν ημέρα (t % DAYSEC), διαιρώντας είναι απλή υπόθεση να βρεθεί η ώρα, το λεπτό και το δευτερόλεπτο.

Οι υπόλοιπες συναρτήσεις στο klibc/time.c είναι πολύ απλές. Η time προσθέτει τα δευτερόλεπτα που έχουν περάσει από την εκκίνηση του πυρήνα με τα δευτερόλεπτα που έχουμε μετρήσει με τον timer για να μας επιστρέψει δευτερόλεπτα από το epoch. Ενώ η asctime δέχεται ένα struct tm, και μας επιστρέφει ένα string με την ημερομηνία και ώρα σε human-readable μορφή.

Δοκιμές με τον χρόνο

Για να βεβαιωθούμε ότι δουλεύουν όλα τα παραπάνω, καταρχάς η init_rtc τυπώνει την αρχική ημερομηνία και ώρα που παίρνει διαβάζοντας το RTC. Επιπλέον τροποποιούμε τον timer interrupt handler που είδαμε παραπάνω, ώστε σε κάθε δευτερόλεπτο που περνά να μας λέει την παρούσα ημερομηνία και ώρα.

static void intr_handler()
{
    if(++nticks % TICK_FREQ_HZ == 0) {
        time_t t = time(0);
        printf("%s", asctime(gmtime(&t)));
    }
}

Αν όλα βαίνουν καλώς, πρέπει να δούμε κάτι σαν αυτό που δείχνει η εικόνα 1.

timer/rtc output picture
Εικόνα 1: Αποτέλεσμα της εκτέλεσης του kernel.

Ασκήσεις για τον αναγνώστη

Είπαμε προηγουμένως ότι ο counter 2 του timer είναι συνδεδεμένος στο PC speaker, για να μας επιτρέπει να δημιουργούμε ήχους σε διάφορες συχνότητες. Δοκιμάστε να υλοποιήσετε μια συνάρτηση beep(int frequency, int duration), και χρησιμοποιήστε την για να παίξετε μια μικρή μελωδία από διαφορετικούς τόνους. Πέρα από τον προγραμματισμό του timer που εξηγήσαμε παραπάνω, θα χρειαστεί να θέσετε τα bits 0 και 1 στο port 0x61 του keyboard controller για να αρχίσει να βγάζει ήχο το speaker, ενώ μηδενίζοντας τα σταματάει.


Creative Commons License
Copyright © John Tsiombikas 2011
This article is licensed under the Creative Commons Attribution-ShareAlike 3.0 License.