TinyOS:nesC Programming Language
Από DistrSys
Πίνακας περιεχομένων |
Η γλώσσα προγραμματισμού nesC
Η nesC (προφέρεται Nessie, όπως το τέρας στη λίμνη του Λοχ Νες :) ) είναι μια γλώσσα προγραμματισμού, η οποία αναπτύχθηκε στο πανεπιστήμιο της Καλιφόρνια στο Berkeley σε συνεργασία με την ομάδα που δημιούργησε το TinyOS. Όπως το TinyOS, έτσι και η nesC είναι ένα open-source project και όποιος επιθυμεί να δει τον κώδικα για το μεταγλωττιστή της nesC για το TinyOS ή να τον τροποποιήσει, μπορεί να επισκεφθεί τη σχετική ιστοσελίδα nesC για να προμηθευθεί τα αντίστοιχα αρχεία.
Η nesC χρησιμοποιείται ως το επίσημο προγραμματιστικό παράδειγμα για το TinyOS από την έκδοση 1.0 του λειτουργικού και μετά. Μάλιστα, το TinyOS 1.0 γράφτηκε από την αρχή σε nesC. Πριν από τη nesC χρησιμοποιούταν ένα μίγμα από καθαρή C και πολλές μακρο-εντολές. Το αποτέλεσμα ήταν ότι υπήρχαν προβλήματα ασάφειας και debugging και επίσης ο κώδικας που γραφόταν δεν ήταν ιδιαίτερα κατανοητός. Αντίθετα, η nesC αντιμετωπίζει το πρόβλημα της συγγραφής κώδικα για εφαρμογές TinyOS με ένα αρκετά κομψό και αποδοτικό τρόπο. Για το λόγο αυτό δε θα ασχοληθούμε καθόλου με το προηγούμενο προγραμματιστικό παράδειγμα για το TinyOS. Βέβαια, υπάρχει μια περίοδος μέχρι ο προγραμματιστής να προσαρμοστεί στη νέα γλώσσα. Οι παρουσιάσεις των φροντιστηρίων του μαθήματος είναι μία καλή εισαγωγή και θα σας φανούν χρήσιμες.
Μερικές από τις αρχές που διέπουν τη σχεδίαση της nesC είναι:
1. Η nesC βασίζεται στη C: Αυτή η επιλογή έγινε διότι οι μεταγλωττιστές της C παράγουν αποδοτικό κώδικα για όλους του microcontrollers που μπορούν να χρησιμοποιηθούν ως επεξεργαστές σε συστήματα έξυπνης σκόνης. Επίσης, ένα μεγάλο μέρος των προγραμματιστών σε embedded συστήματα χρησιμοποιεί τη C ως γλώσσα προγραμματισμού.
2. Ολική ανάλυση προγράμματος κατά τη διάρκεια της μεταγλώττισης (compiling): Η ξεχωριστή μεταγλώττιση μερών ενός προγράμματος δεν είναι διαθέσιμη ως επιλογή στη nesC. Αυτό γίνεται για να είναι πιο ακριβής ο έλεγχος για λάθη και συγχρονισμό (race conditions) στο πρόγραμμα και για πιο αποδοτικό κώδικα (μικρότερο μέγεθος), το οποίο είναι αρκετά βολικό, δεδομένου του μικρού μεγέθους της διαθέσιμης μνήμης.
3. Στατικός κώδικας: Η μνήμη κατανέμεται στατικά σε μια εφαρμογή TOS κατά τη διάρκεια της μεταγλώττισης και επίσης το γράφημα διασυνδέσεων μεταξύ των διάφορων component είναι σταθερό και γνωστό. Το μοντέλο των component εξαλείφει την ανάγκη για δυναμική δέσμευση μνήμης και ενθαρρύνει ένα ευέλικτο σχεδιασμό.
4. Η nesC υποστηρίζει και αντικατοπτρίζει τη φιλοσοφία του TinyOS.
Modules και Interfaces
Προχωράμε τώρα στα ενδότερα της γλώσσας. Όπως είπαμε, μια εφαρμογή TOS αποτελείται από διάφορα components, τα οποία συνδέονται μεταξύ τους με κάποιο συγκεκριμένο τρόπο. Υπάρχουν μόνο δύο είδη component, τα πρώτα ονομάζονται module και τα δεύτερα configuration. Κάπου εδώ η κατάσταση περιπλέκεται, οπότε ζητώ την κατανόηση του αναγνώστη. Οι τρεις βασικές έννοιες στις οποίες πρέπει να δώσει σημασία και να κατανοήσει είναι module, configuration και interface.
Τα module λοιπόν υλοποιούν τη λογική που αλλάζει την κατάσταση του αυτόματου, στο οποίο αντιστοιχεί ουσιαστικά ένα component. Αυτό γίνεται με κώδικα που μοιάζει πολύ με C. Πρακτικά αυτό σημαίνει ότι υλοποιούν τα interface, τα οποία έχουν δηλώσει ότι χρησιμοποιούν. Πριν να αναλύσουμε την έννοια του interface, να πούμε ότι η αντιστοίχηση interface στα modules, δηλαδή ποια interface χρησιμοποιεί ένα module, γίνεται με ένα configuration. Προχωράμε τώρα να αποσαφηνίσουμε τις παραπάνω έννοιες.
Ένα interface χρησιμοποιείται για να ορίσει τον τρόπο που επικοινωνούν δύο modules. Από τα δύο modules, το ένα ονομάζεται χρήστης (user) και το άλλο παροχέας (provider). Αυτή η διάκριση γίνεται για να δείξει μάλλον τη ροή του ελέγχου. Για να γίνει αυτό πιο κατανοητό, έστω ότι έχουμε ένα module πολύ γενικό και αφηρημένο. Για να μπορέσει να εκτελέσει μια λειτουργία χρειάζεται τις υπηρεσίες από άλλα modules, τα οποία είναι λιγότερο γενικά και πιο κοντά ας πούμε στο hardware. Έτσι, το πρώτο module θα χρησιμοποιήσει το interface με το οποίο συνδέεται με ένα module πιο κάτω στην ιεραρχία. Το πρώτο module θα γίνει user και το δεύτερο provider. Στο παράδειγμα που είδαμε στην προηγούμενη ενότητα, το module Sense θα χρησιμοποιήσει το interface ADC, το οποίο του παρέχει το module Photo. Η κατεύθυνση του βέλους κάνει σαφή τη διάκριση αυτή.
Τα interface είναι ο μόνος τρόπος αλληλεπίδρασης μεταξύ των modules και περιέχουν εντολές (commands) και γεγονότα (events). Οι εντολές πρέπει να υλοποιηθούν από τη μεριά του provider ενώ τα events από τη μεριά του user. Οι εντολές μας επιτρέπουν να ελέγξουμε ένα module, ενώ τα events είναι οι αντιδράσεις που προκύπτουν από την εκτέλεση των εντολών. Αυτό ίσως να μην φαίνεται λογικό με μια πρώτη ματιά αλλά σκεφθείτε το ως εξής: Τα interface είναι ανεξάρτητα από τα components. Δεν περιέχουν κώδικα ο οποίος υλοποιεί κάποια λογική. Απλά καθορίζουν τον τρόπο επικοινωνίας ανάμεσα στα modules.
Ακολουθούν οι ορισμοί των interfaces που χρησιμοποιεί το module Sense. Βλέπουμε ότι απλά ορίζουν τις εντολές και τα events που χρησιμοποιούν.
interface Timer {
command result_t start(char type, uint32_t interval);
command result_t stop();
event result_t fired();
}
interface SendMsg {
command result_t send(TOS_Msg *msg, uint16_t length);
event result_t sendDone(TOS_Msg *msg, result_t success);
}
interface ADC {
command result_t getData();
event result_t dataReady(uint16_t data);
}
Για το παράδειγμά μας, στο interface ADC η εντολή getData() θα πρέπει να υλοποιηθεί από το module Photo και το event dataReady θα πρέπει να υλοποιηθεί από το module Sense. Ακολουθεί μέρος του ορισμού των module αυτών.
module Sense {
uses interface ADC;
uses interface Timer;
uses interface Send;
}
implementation {
uint16_t sensorReading;
event result_t Timer.fired() {
call ADC.getData();
return SUCCESS;
}
event result_t ADC.dataReady(uint16_t data) {
sensorReading = data;
...
return SUCCESS;
}
}
module Photo{
provides interface ADC;
}
implementation {
...
command result_t getData() {
...
}
}
Τώρα η εύλογη απορία είναι πώς καλούνται αυτά τα events και οι εντολές που παρέχει ένα interface. Ας ξαναδούμε την διαδικασία που περιγράψαμε στο παράδειγμα με το Sense.
Configuration και Συνδεσμολογία
Επιστρέφουμε τώρα στην έννοια του configuration. Ένα configuration είναι ένα component που λεει στον μεταγλωττιστή ποια interface χρησιμοποιούμε στην εφαρμογή μας, μεταξύ ποιων modules χρησιμοποιούνται και ποιοι είναι οι ρόλοι, user ή provider. Λογικά, ένα configuration σχηματίζει ένα γράφημα όπως αυτό στο σχήμα που ακολουθεί. Έστω ότι το module sense αποφασίζει ότι θέλει ένα δείγμα από τον αισθητήρα φωτός (module Photo). Θα καλέσει την εντολή getData(), ως εξής:
call ADC.getData();
δηλαδή αναφέρεται στο interface που χρησιμοποιεί (ADC) και στην εντολή getData ως συνάρτηση μέλος ενός αντικειμένου ADC. Όταν ληφθεί το δείγμα, πρέπει να ειδοποιηθεί το module Sense για να το καταγράψει σε μία μεταβλητή. Αυτό θα γίνει με τη χρήση ενός event, το οποίο θα κληθεί μέσα στο module Photo ως εξής:
signal ADC.dataReady( SensorReadings);
Το διάγραμμα που αντιστοιχεί στο configuration του Sense φαίνεται στο σχήμα \ref{sense_diagram}. Από το γράφημα αυτό, ο μεταγλωττιστής ξέρει ότι το ADC συνδέει τα Sense και Photo, οπότε μπορεί να καταλάβει πως θα γίνει η σύνδεση των αντίστοιχων συναρτήσεων.
Ακόμα, στο σχήμα αυτό περιέχεται και ένα module για το οποίο δεν έχουμε μιλήσει ακόμα, το Main, το οποίο περιέχεται σε κάθε εφαρμογή TOS και χρησιμοποιεί το interface StdControl. Αυτό το interface χρησιμοποιείται για να αρχικοποιήσει όλα τα modules της εφαρμογής, είναι κάτι σαν ένα γενικό κουμπί reset με άλλα λόγια.
Κάτι άλλο που πρέπει να παρατηρήσουμε είναι η σύμβαση που χρησιμοποιείται σε τέτοια γραφήματα από τους συγγραφείς του TinyOS, δηλαδή ότι τα μαύρα τρίγωνα σε ένα module δηλώνουν commands, ενώ τα λευκά δηλώνουν events. Παρατηρούμε ότι το interface StdControl έχει μόνο μία εντολή, αφού απλά χρειάζεται να κάνει reset στα modules.
Θα δούμε τώρα πως ορίζεται το configuration για την εφαρμογή Sense. Έχουμε:
configuration Sense { }
implementation {
components Main, SenseM, Photo, TimerC, NetProto;
Main.StdControl -> SenseM.StdControl;
Main.StdControl -> Photo.StdControl;
Main.StdControl -> TimerC.StdControl;
Main.StdControl -> NetProto.StdControl;
SenseM.ADC -> Photo.ADC;
SenseM.Timer-> TimerC.Timer;
...
}
Παρατηρούμε ότι ένα configuration δεν περιέχει καθόλου κώδικα για κάποια εφαρμογή και ότι στο implementation μέρος έχει μια σειρά από δηλώσεις που δείχνουν τη σχέση μεταξύ δύο components. Αρχικά, δηλώνουμε ποια components θα χρησιμοποιήσουμε στην εφαρμογή μας. Όπως αναφέρθηκε, υπάρχει πάντοτε ένα component με το όνομα Main, το οποίο χρησιμοποιείται απλά για να αρχικοποιήσουμε το σύστημα μέσω του interface StdControl. Προφανώς το όνομα δείχνει τη σχέση με τη συνάρτηση main() που χρησιμοποιείται στη C.
Το βέλος που χρησιμοποιείται στις δηλώσεις μέσα σε ένα configuration δείχνει ποιος είναι ο provider και ποιος ο user ανάμεσα σε δύο components. Στα αριστερά του βέλους είναι το component που χρησιμοποιεί το interface, ενώ στα δεξιά αυτό που το παρέχει.
Θα προχωρήσουμε τώρα ένα βήμα παραπέρα και θα ανακαλύψουμε ότι στην πραγματικότητα, το component TimerC είναι configuration και όχι module! Συγκεκριμένα, τα configuration μπορούν και αυτά να παρέχουν και να χρησιμοποιούν interface! Στο σχήμα που ακολουθεί φαίνεται ότι αποτελείται από δύο module, το TimerM και το HWClock, τα οποία συνδέονται μεταξύ τους με ένα clock interface.
Ο ορισμός του configuration TimerC είναι ο εξής:
configuration TimerC {
provides {
interface StdControl;
interface Timer;
}
} implementation {
components TimerM, HWClock;
StdControl = TimerM.Stdcontrol;
Timer = TimerM.Timer;
TimerM.Clk -> HWClock.Clock;
}
Παρατηρούμε ότι σε δύο δηλώσεις αντί για το βέλος που είδαμε πριν, χρησιμοποιείται το ίσον. Αυτό γίνεται επειδή θέλουμε να αντιστοιχήσουμε τα interface που παρέχει το component στα interface τα οποία παρέχει το TimerM. Δεν παρεμβάλλεται κάποιο component μεταξύ τους, οπότε είναι ουσιαστικά τα ίδια interface.
Υπάρχουν πολλά πράγματα ακόμα να ανακαλύψει κάποιος στη nesC. Μία πρώτη καλή κίνηση είναι να κοιτάξετε τα tutorials που περιέχονται στη διανομή του TinyOS, στα οποία αναλύεται βήμα προς βήμα πως γράφεται μια εφαρμογή TOS. Δύο ακόμα καλές πηγές είναι το Reference manual της nesC και η δημοσίευση των δημιουργών της σχετικά με το θέμα. Η nesC είναι σχετικά μικρή σε μέγεθος γλώσσα, αλλά έχει πολλά λεπτά σημεία που απαιτούν την προσοχή σας.
Ένα ακόμα απλό παράδειγμα εφαρμογής TOS
Θα στηριχθούμε σε δύο έτοιμες εφαρμογές, τις CntToRfmAndLeds και RfmToLeds, τις οποίες στη συνέχεια θα τροποποιήσουμε. Επίσης, θα δούμε αναλυτικά τον κώδικα για αυτές τις δυο εφαρμογές προκειμένου να γίνει κατανοητή η διαδίκασια της σχεδίασης μιας εφαρμογής TOS.
Ξεκινούμε με την CntToRfmAndLeds, η οποία περιέχεται στον κατάλογο TinyOS-1.x/apps/CntToRfmAndLeds και η λειτουργία της είναι να εμφανίζει τα τελευταία 3 bit από ένα counter 16 bit στα 3 LEDs που υπάρχουν διαθέσιμα στα mica motes, και να στέλνει ένα μήνυμα με την τιμή του counter σε όλους τους κόμβους γύρω του (broadcast). Σύμφωνα με τη σύμβαση που ακολουθείται στο TinyOS, το wiring για την εφαρμογή περιέχεται σε ένα αρχείο με όνομα ίδιο με αυτό της εφαρμογής, δηλαδή στο CntToRfmAndLeds.nc. Η συγκεκριμένη εφαρμογή είναι απλά ένα configuration, το οποίο συνδέει έτοιμα components από τη βιβλιοθήκη του TinyOS, συγκεκριμένα τα Counter, IntToLeds, IntToRfm και TimerC. Ο αντίστοιχος κώδικας είναι:
configuration CntToLedsAndRfm {
}
implementation {
components Main, Counter, IntToLeds, IntToRfm, TimerC;
Main.StdControl -> Counter.StdControl;
Main.StdControl -> IntToLeds.StdControl;
Main.StdControl -> IntToRfm.StdControl;
Counter.Timer -> TimerC.Timer[unique("Timer")];
Counter.IntOutput -> IntToLeds;
Counter.IntOutput -> IntToRfm;
}
}
Παρατηρούμε ότι δηλώνουμε και το component Main, το οποίο είναι το component από το οποίο ξεκινά η εκτέλεση και αρχικοποιεί τα υπόλοιπα components μέσω πολλαπλής σύνδεσης του Main.StdControl interface. Ας δούμε τώρα τι κάνει το κάθε component ξεχωριστά.
Το component TimerC είναι μια λογική αφαίρεση είναι hardware timer, και μπορούμε σε μια εφαρμογή να έχουμε 256 διαφορετικά στιγμιότυπα του component αυτού. Για το λόγο αυτό, στη δήλωση του wiring χρησιμοποιούμε το όρισμα unique για να είμαστε σίγουροι ότι κάθε φορά που το καλούμε δημιουργούμε ένα καινούριο στιγμιότυπο (προφανώς εδώ αυτό δεν έχει ιδιαίτερη χρησιμότητα, σε άλλες εφαρμογές όμως με πολλαπλούς timers τα πράγματα είναι διαφορετικά). Παρέχει ένα απλό interface που αρχίζει αντίστροφη μέτρηση από κάποια τιμή και όταν τελειώσει επιστρέφει ένα event.
To event αυτό το χρησιμοποιεί το Counter component, για να αυξήσει μια 16 bit τιμή, την οποία και στέλνει στα components IntToLeds και IntToRfm, μέσω του interface IntOutput, που επιτελεί ακριβώς αυτήν την εργασία. Το IntToLeds απλώς διαβάζει αυτή την τιμή και εμφανίζει τα τελευταία 3 bit στα LEDs του mica mote.
To IntToRfm αντίθετα κάνει κάτι πολύ πιο ενδιαφέρον: στέλνει την τιμή που διαβάζει από τον Counter στο δίκτυο με ένα radio broadcast. Πρέπει όμως πρώτα να δούμε πώς γίνεται η επικοινωνία με μηνύματα μεταξύ των κόμβων.
Ένα μήνυμα στο TinyOS αντιπροσωπεύεται από μία δομή, την TOS_Msg, η οποία ορίζεται στο αρχείο tos/system/AM.h. Περιέχει πεδία για τη διεύθυνση του παραλήπτη, ένα πεδίο που ονομάζεται handlerID, ένα groupID, μήκος μηνύματος και payload, δηλαδή την πληροφορία που περιέχει το μήνυμα. Για τη διέυθυνση του παραλήπτη έχουμε τρεις επιλογές:
- Να βάλουμε ως διεύθυνση το moteID ενός συγκεκριμένου κόμβου στον οποίο θέλουμε να μεταδώσουμε το μήνυμα. Το moteID προγραμματίζεται σε έναν κόμβο κατά τον προγραμματισμό της flash του, ή αν κάνουμε εξομοίωση μέσω TOSSIM του ανατίθεται αυτόματα ένα ID.
- Να βάλουμε ως διεύθυνση την TOS_BCAST_ADDR, που σημαίνει broadcast στο δίκτυο οπότε το μήνυμα απευθύνεται σε όλους τους κόμβους.
- Να βάλουμε τη διεύθυνση TOS_UART_ADDR, με άλλα λόγια μετάδοση από τη σειραϊκή θύρα.
Επίσης, υπάρχει και η global μεταβλητή TOS_LOCAL_ADDR, που είναι το moteID του κόμβου μας.
Το handlerID από την άλλη, είναι κάτι αντίστοιχο με τα TCP ports, δηλαδή όταν ένας κόμβος λάβει ένα μήνυμα, θα το προωθήσει στο component που δήλωσε ότι δέχεται μηνύματα με αυτό το handlerID. Δίνεται με αυτόν τον τρόπο η δυνατότητα να έχουμε ταυτόχρονα διάφορα είδη μηνυμάτων, τη διαχείριση των οποίων αναλαμβάνουν αυτόματα διαφορετικά μέρη μιας εφαρμογής TOS. Ας δούμε πώς γίνεται αυτό στον κώδικα για το configuration IntToRfm:
configuration IntToRfm
{
provides interface IntOutput;
provides interface StdControl;
}
implementation
{
components IntToRfmM, GenericComm as Comm;
IntOutput = IntToRfmM;
StdControl = IntToRfmM;
IntToRfmM.Send -> Comm.SendMsg[AM_INTMSG];
IntToRfmM.StdControl -> Comm;
}
}
To GenericComm είναι το component που αναλαμβάνει τη μετάδοση μηνυμάτων, είτε αυτή είναι μέσω radio είτε μέσω του serial port. Συνδέουμε το module IntToRfmM με το GenericComm μέσω του interface Send, το οποίο αρχικοποιείται ως instance με τον αριθμό AM_INTMSG, ο οποίος είναι μια σταθερά σε ένα include αρχείο, και είναι συγκεκριμένος για την εφαρμογή μας. Τα μηνύματα που στέλνονται μέσω του interface αυτού θα έχουν ως handlerID τον αριθμό AM_INTMSG.
To groupID επιτρέπει σε επιλεγμένους κόμβους να ακούν το ίδιο μήνυμα, ουσιαστικά ένα broadcast σε περιορισμένο σύνολο κόμβων. Το groupID ενός κόμβου προγραμματίζεται θέτοντας την επιθυμητή τιμή στο Makefile της εφαρμογής. Π.χ αν θέλουμε να δώσουμε σε ένα mote groupID 13, εισάγουμε στο Makefile τη γραμμή DEFAULT_LOCAL_GROUP = 0x0D. Η default τιμή είναι 0x7D (δηλαδή 125).
Στο παρακάτω τμήμα κώδικα βλέπουμε πως σχηματίζουμε ένα μήνυμα προς μετάδοση. Χρησιμοποιούμε μια δομή IntMsg για το payload μέρος του μηνύματος, η οποία δομή απλά περιέχει μια τιμή του counter και το moteID του πομπού. Καλούμε έπειτα την εντολή send του interface.Send για να κάνουμε bradcast το μήνυμα.
bool pending;
struct TOS_Msg data;
/* ... */
command result_t IntOutput.output(uint16_t value) {
IntMsg *message = (IntMsg *)data.data;
if (!pending) {
pending = TRUE;
message->val = value;
message->src = TOS_LOCAL_ADDRESS;
if (call Send.send(TOS_BCAST_ADDR, sizeof(IntMsg), &data))
return SUCCESS;
pending = FALSE;
}
return FAIL;
}
Η δεύτερη εφαρμογή που θα μας απασχολήσει είναι η RfmToLeds, που παίζει το ρόλο του δέκτη των μηνυμάτων που στέλνει ένα mote με την εφαρμογή CntToRfmAndLeds. Η λειτουργία που επιτελεί είναι ουσιαστικά η λήψη των μηνυμάτων αυτών και να τα στέλνει στο component που διαχειρίζεται τα μηνύματα με handlerID AM_INTMSG, συγκεκριμένα στο component RfmToInt. Αυτό με τη σειρά του, εξάγει από τα μηνύματα την τιμή του counter που περιέχουν και την προωθεί σε άλλο component, με το οποίο μπορούμε να εμφανίσουμε την τιμή αυτή στα LEDs του mote. Οπότε, προγραμματίζοντας ένα mote με την εφαρμογή CntToRfmAndLeds και ένα άλλο με την RfmToLeds, έχουμε αμέσως ασύρματη επικοινωνία :)



