Mehr Sicherheit in der Programmierung Ada für Embedded

Beispiel 4-Bit-Integer definieren

Einige Beispiele sollen dies verdeutlichen. Die erste Definition ist ein Beispiel, ein 4-Bit-Integer von 1 bis 16 zu definieren:
type Field is range 1 .. 16 with Size => 4;

Dabei ist zu beachten, dass wir diesem Typ nur 4 Bit zugestehen, also zum Beispiel die Zahl 16 gar nicht direkt dargestellt werden kann. Da aber der Datentyp 16 Werte darstellen kann, und wir auch genau 16 Werte brauchen – und zwar von 1 bis 16 und nicht von 0 bis 15 –, ist das kein Problem, und der Compiler kann eventuell nötige Anpassungen der Werte in Berechnungen selbst durchführen.

Ein komplexeres Beispiel, diesmal mit Strukturen – Records in Ada. Wir definieren zuerst Datentypen für drei Felder eines solchen Records, mit unterschiedlichen Größen:

type Field1 is … with Size => 5;

type Field2 is … with Size => 15;

type Field3 is … with Size => 12;

Und dann den Record-Typ selbst:

type The_Register is record

F1 : Field1;

F2 : Field2;

F3 : Field3;

end record with Size => 32,

Volatile_Full_Access,

Bit_Order => System.Low_Order_First; 

for The_Register use record

F1 at 0 range 0 .. 4;

F2 at 0 range 5 .. 19;

F3 at 0 range 20 .. 31;

end record; 

declare Register : The_Register with Address => 16#800012AF#;

Begin Register.F2 := 5;

end;
Eine »normale« Definition eines Record-Typen in Ada würde nur den ersten Teil enthalten, in dem der Typ selbst und seine Felder definiert werden. Der zweite Teil ist die Repräsentationsklausel, die definiert, wo genau die Felder im Speicher liegen sollen. Die Bit-Order-Instruktion weist den Compiler an, die Daten immer in Little-Endian-Format darzustellen, auch wenn man sich auf einem Big-Endian-System befindet. Damit sind schwierig zu findende Fehler, die aus Endianness-Problemen resultieren, ausgeschlossen. Der Code am Ende deklariert eine Variable dieses Typs und weist ihr auch eine bestimmte Adresse im Speicher zu, um sie auf ein Register des Prozessors abzubilden. Wie man sehen kann, wird für die folgende Zuweisung zu dem Record-Feld F2 keine Bitmanipulationen benötigt.

Objektorientierung ist schon seit langem Mainstream – außer in der Embedded-Programmierung, wo oft noch prozedural programmiert wird. Der Grund dafür ist, dass in vielen Sprachen Objektorientierung an einen Garbage-Collector geknüpft ist, und oft auch mit Performance-Einbußen oder wenig vorhersagbaren Ausführungszeiten einhergeht. In Ada ist beides nicht der Fall, durch den Einsatz von statischen Dispatching-Tables sowie sogenannten Storage-pools als Ersatz für Garbage-Collection.

Ada gehört auch zu den wenigen Sprachen, die Nebenläufigkeit direkt in die Sprache integrieren. In den meisten Fällen wird diese Funktionalität durch eine Thread-Bibliothek oder durch das Betriebssystem bereitgestellt. In Ada können direkt sogenannte Tasks definiert werden, die dann nebenläufig ausgeführt werden. Kommunikation erfolgt synchron durch sogenannte Rendezvous-Aufrufe, oder asynchron durch sogenannte protected objects, ein Sprachkonstrukt, das ähnlich wie in der Objektorientierung Daten und Funktionen, die diese Daten manipulieren, in einer auch im parallelen Kontext sicheren Weise kombiniert. Ada-Tasks funktionieren auch im sogenannten Bare-Metal-Konzept, das heißt auch ohne Echtzeitbetriebssystem, wenn die Ada-Runtime auf das Zielsystem angepasst wurde.

Unter vertragsbasierter Programmierung versteht man Techniken, die die klassische Funktionsdeklaration bereichern und damit deutlicher machen, welche Aufgaben der aufrufenden, und welche Aufgaben der aufgerufenen Funktion zugeordnet werden sollen; das Konzept ähnelt einem Vertrag im echten Leben. Zum Beispiel herrscht oft Unklarheit darüber, ob eine C-Funktion, die einen Zeiger als Argument erwartet, auch funktionieren soll, wenn der Null-Zeiger übergeben wird, oder ob der Aufrufende diese Situation ausschließen soll. Ein anderes Beispiel sind erwartete Wertebereiche zum Beispiel eines Winkels oder einer Temperatur, die oft in Kommentaren dokumentiert werden.