10
Introduction to ADA and its use for embedded applications From July 1987, ADA becomes the UK Ministry of Defence's single preferred language for realtime contracts. John Barnes examines the software engineering features that make ADA an important practical advance on earlier languages The paper gives a brief overview of the key features of ADA and elaborates on those aspects which are particularly relevant to embedded applications. ADA was originally developed on behalf of the US Department of Defense for embedded systems which, in the early 1970s, were perceived as having a high and escalating cost. After a summary of the background to the language, the main technical reasons why AOA is SUCh an important practical advance are outlined. Some of the requirements of realtime embedded systems are then recalled. The features of ADA aimed at such systems are described in some detail and their use is illustrated with a number of examples. The paper concludes by commenting on how well AOA meets the requirements in theory and some remarks on experience with ADA to date. high-level languages ADA embedded systems software engineering ADA was developed because the US Department of Defense (DoD) needed to contain its ever increasing software development and maintenance costs. The DoD was not alone in recognizing the problems but it was perhaps uniquely placed in its ability to drive forward a worldwide development programme to do something about them. ADA was developed primarily for the embedded system sector as described in the Steelman requirements document 1. Although that sector does have some special demands, many of its needs are common to programming in general. In the event, ADA is a general- purpose procedural language and is as suited to scientific, engineering and commercial areas as it is to realtime control. Indeed, it might be argued that ADA has already proved itself much better than FORTRAN, COBOL and SlMULA in their respective areas but remains to do so in the realtime area. The timing of the development of ADA was fortunate. Much had been learnt in the 1960s and 1970s about the Alsys Ltd, Partridge House, Newtown Road, Henley-on-Thames, Oxon RG9 1EN, UK 0141-9331/87/05245-10 $03.00 © Vol "11 No 5 June "1987 concepts of programming and software engineering. Many new concepts had emerged and been proven in a number of little known research languages. However, the existing practical languages were still those that had been conceived in the 1950s and added to, and the DoD development took the opportunity to bring all these concepts together into a single practical language. Such opportunities do not arise often, and it is possible that ADA will be the last general-purpose language of its kind- certainly in this century and a long way into the next. Of course, ADA will evolve slightly in years to come through the normal process of standardization and revision, but the main shape and details are here to stay. There are two major reasons why ADA is important. The first is that it is a standard. In fact, it is the only international standard there has ever been in the embedded systems area. Since ADA is a general-purpose language, given the right infrastructure it should steadily become the standard for all programming of a general-purpose nature including those areas currently dominated by FORTRAN and COBOL. The other important feature of ADA is that it is consistent with the needs of software engineering. By comparison with other branches of engineering, little is known of the underlying science of software, if indeed there is such a thing. The type 'integer' perhaps has some firm mathematical foundation, but most of the rest is contrived. Nevertheless, design, decomposition and understanding have evolved significantly during recent years and some consensus has been reached. ADA fits in well with such matters as compared with previous languages and provides a much needed firm foundation on which to build. ADA AND SOFTWARE ENGINEERING Two key principles seem to underlie engineering disciplines. One is the development of standard methods so that each project does not have to conceive from scratch how it should be done. The other is the development of reusable parts so that commonly recurring units need not be redesigned each time they are used. These two 1987 Butte~vorth & Co. (Publishers) Ltd 245

Introduction to ADA and its use for embedded applications

Embed Size (px)

Citation preview

Page 1: Introduction to ADA and its use for embedded applications

Introduction to ADA and its use for embedded applications

From July 1987, ADA becomes the UK Ministry of Defence's single preferred language for realtime contracts. John Barnes examines the software engineering features that make ADA an important practical

advance on earlier languages

The paper gives a brief overview of the key features of ADA and elaborates on t hose aspec ts which are particularly relevant to embedded applications. ADA was originally developed on behalf of the US Department of Defense for embedded systems which, in the early 1970s, were perceived as having a high and escalating cost. After a summary of the background to the language, the main technical reasons why AOA is SUCh an important practical advance are outlined. Some of the requirements of realtime embedded sys tems are then recalled. The features of ADA aimed at such systems are described in s o m e detail and their use is illustrated with a number of examples. The paper concludes by commenting on h o w well AOA meets the requirements in theory and s o m e remarks on experience with ADA to date.

high-level languages ADA embedded systems software engineering

ADA was developed because the US Department of Defense (DoD) needed to contain its ever increasing software development and maintenance costs. The DoD was not alone in recognizing the problems but it was perhaps uniquely placed in its ability to drive forward a worldwide development programme to do something about them. ADA was developed primarily for the embedded system sector as described in the Steelman requirements document 1. Although that sector does have some special demands, many of its needs are common to programming in general. In the event, ADA is a general- purpose procedural language and is as suited to scientific, engineering and commercial areas as it is to realtime control. Indeed, it might be argued that ADA has already proved itself much better than FORTRAN, COBOL and SlMULA in their respective areas but remains to do so in the realtime area.

The timing of the development of ADA was fortunate. Much had been learnt in the 1960s and 1970s about the

Alsys Ltd, Partridge House, Newtown Road, Henley-on-Thames, Oxon RG9 1EN, UK

0141-9331/87/05245-10 $03.00 ©

Vol "11 No 5 June "1987

concepts of programming and software engineering. Many new concepts had emerged and been proven in a number of little known research languages. However, the existing practical languages were still those that had been conceived in the 1950s and added to, and the DoD development took the opportunity to bring all these concepts together into a single practical language. Such opportunities do not arise often, and it is possible that ADA will be the last general-purpose language of its k i n d - certainly in this century and a long way into the next. Of course, ADA will evolve slightly in years to come through the normal process of standardization and revision, but the main shape and details are here to stay.

There are two major reasons why ADA is important. The first is that it is a standard. In fact, it is the only international standard there has ever been in the embedded systems area. Since ADA is a general-purpose language, given the right infrastructure it should steadily become the standard for all programming of a general-purpose nature including those areas currently dominated by FORTRAN and COBOL.

The other important feature of ADA is that it is consistent with the needs of software engineering. By comparison with other branches of engineering, little is known of the underlying science of software, if indeed there is such a thing. The type 'integer' perhaps has some firm mathematical foundation, but most of the rest is contrived. Nevertheless, design, decomposition and understanding have evolved significantly during recent years and some consensus has been reached. ADA fits in well with such matters as compared with previous languages and provides a much needed firm foundation on which to build.

ADA AND SOFTWARE ENGINEERING

Two key principles seem to underlie engineering disciplines. One is the development of standard methods so that each project does not have to conceive from scratch how it should be done. The other is the development of reusable parts so that commonly recurring units need not be redesigned each time they are used. These two

1987 Butte~vorth & Co. (Publishers) Ltd

245

Page 2: Introduction to ADA and its use for embedded applications

themes occur in software engineering. Thus Mascot, for example, has much to do with standard design methods and it is clear that simple subroutine libraries provide a starting point for reusable software components.

Ultimately, the problems of defining interfaces precisely must be controlled. If we can define the interface between two components, then they can be designed, built and tested independently of each other. Many of the problems of software arise because interfaces are not properly defined and because what should be inde- pendent parts do in fact interfere with each other. One of the secrets of defining interfaces is to hide information, and indeed many of the advances in the evolution of programming languages have been to do with hiding information. This is sometimes referred to as abstraction. To illustrate the point, consider some earlier languages.

Until the mid 1950s all programming was in some form of machine code. It is now well recognized that pro- gramming at this level is a time-consuming and error- prone process. Why is this? The main reason is that every fine detail of machine use has to be prescribed precisely by the programmer. The introduction of FORTRAN was a revolution. It removed completely the need for the programmer to consider the use of the machine's registers. A whole class of errors could no longer be made and the programmer was able to express a formula in a way which was meaningful to others who might read the program. The details of register usage were hidden; it was possible to write an abstract mathematical expression and let the compiler take care of it.

Another step of a similar kind came with languages such as ALGOL 60 and derivatives such as CORAL 66. The if- then-else constructions which make the flow of control more discernable are really another form of abstraction, hiding the labels and 'goto's of other languages. Dijkstra introduced the notion that the 'goto' was harmful; this was the start of 'structured programming' and perhaps the beginning of software engineering. Hiding labels and 'goto's removes a whole class of possible errors and makes the program more readable.

It is clear, therefore, that techniques for hiding information are of great value. Probably the most important feature of ADA is the package; its main property is to keep certain information hidden from those parts of a program that do not need to know it. As a trivial example, consider a simple piece of programming that represents how a stack and access routines to it might be coded:

MAX: constant: = 100; S: array(1 .. MAX) of INTEGER; TOP: INTEGER range 0.. MAX;

procedure PUSH(X: INTEGER) is begin

TOP:-- TOP + 1; S(TOP): = X;

end PUSH;

function POP return INTEGER is begin

TOP:= TOP- 1; return S(TOP + 1);

end POP;

This example reveals, already, a number of useful facilities of ADA. Note how MAX is declared to be a constant, i.e. its value cannot be changed after it has been declared, and so an accidental attempt to do so will be detected by the compiler. Hence, another class of error is prevented. Note

also the range prescribed tor TOP; this ettsures ~hdt: al execution time the value of TOP cannot stray outside the given limits.

However, the real purpose of this example is to show that traditional block structured languages (and CORAL ~, falls into this category) have serious problems with control of visibility. We are implementing a stack as the array S and an index TOP. We would like all access to that stack to be via the subprograms, PUSH and POP, but there is no way in which access can be provided to PUSH and POP without at the same time giving direct access to the variables S and TOP. As a consequence, the desired protocol cannot be enforced since unfortunately the malevolent or careless programmer can directly manipulate the fine detail of the stack.

The ADA package overcomes this by allowing a wall to be placed around a group of declarations which only permits access to those which are intended to be visible. A package comes in two parts: the 'specification', which gives the visible information, and the 'body', which contains the hidden details. The above example could thus be recast in the following form:

package STACK is ---specification procedure PUSH(X:INTEGER); function POP return INTEGER;

end;

package body STACK is --body MAX: constant: = 100; S: array (1 .. MAX) of INTEGER; TOP: INTEGER range 0.. MAX:= O;

procedure PUSH(X: INTEGER) is begin

TOP:= TOP + I; S(TOP): = X;

end PUSH;

function POP return INTEGER is begin

TOP:= TOP - 1; return S(TOP + 1);

end POP;

end STACK;

With this construction, the language rules are such that, outside the package, the stack can only be manipulated by calling PUSH and POP. The inner details are quite hidden. This means, for example, that the package could be reimplemented internally by a different technique (using linked lists, perhaps) and we would be assured that no part of the users' programs need be changed, because the user can write nothing that uses variables such as S and TOP. This is obviously a trivial example, but the advantages for program maintenance are clear. Given the proper use of packages, individual parts of a program can be maintained without the fear that other parts might be upset.

In simple cases such as this, hiding can be achieved in some existing languages through the process of separate compilation and external (or equivalent) statements, but the important point is that visibility control is a quite distinct requirement from separate compilation and ADA correctly provides appropriate facilities for each. It is not possible here to illustrate the full power of the ADA package, but there is much more to it than the above example shows.

The subdivision of a large program into separately compiled units is a basic requirement for any serious

246 Microprocessors and Microsystems

Page 3: Introduction to ADA and its use for embedded applications

program development. Many languages (e.g. PASCAL) rather naively ignore this requirement and individual implementers are then forced to extend the language in some way. A common problem with many language implementations is that the interfaces between separately compiled units may not be policed. Thus, in PASCAL it might be possible to declare a procedure with four parameters but, when calling it from some separately compiled code, accidentally supply only three. Each unit is independently correct but they are inconsistent with the others. If a program is built out of such units with a language-insensitive builder then the resulting program will probably run out of control, will fail to satisfy the basic assumptions of any debugging or tracing code, and generally will be very difficult to debug.

The process of separate compilation is well defined in ADA. Separately compiled units are kept in a program library, and the library manager ensures that the interfaces between units are correct. It is thus not possible to build a program out of inconsistent lumps, and so another class of execution errors is not possible. The process is illustrated usingthe STACK package. Suppose we compile STACK (both specification and body). This results in the compiled code going into the program library and also those parts of the symbol table for STACK that relate to its use. To use the STACK, we write something like

with STACK; procedure MY_PROGRAM is

begin

STACK.PUSH (...);

end/~Y--PROG RAM;

When this text is submitted to the compiler, it recognizes the phrase 'with STACK;' and searches for the appropriate symbol tables. The calls of PUSH can then be analysed, checked and compiled correctly.

An important facility is the ability to compile specifica- tion and body separately. However, there is a rule that the body must be compiled after the specification; similarly the user text (MYmPROG RAM in our example) must also be compiled after the specification because the specifica- tion prescribes the interface and this has to be given before the pieces of program which depend upon it. There is, however, no such relation between the body and the user program, which can be compiled (and recompiled) in any order.

The above mechanism allows a library of units to be built for later use. It provides a bottom-up development route. ADA also includes a means for top-down program decomposition in which the interfaces are again checked. In the case of the body of STACK, the majority of the subprograms PUSH and POP could be extracted leaving only their 'stubs'. Thus

package body STACK is MAX: constant:= 100; S: array (1.. MAX) of INTEGER; TOP: INTEGER range 0. . MAX:= 0; procedure PUSH(X:INTEGER) is separate; --stub function POP return INTEGER is separate; --stub

end STACK;

This body can be compiled and placed in the library, and then later provided with the extracted subprograms which then take the form

separate(STACK) procedure PUSH(X: INTEGER) is begin

TOP:= TOP + 1; S(TOP): = X;

end PUSH;

ADA thus has both top-down and bottom-up means of functional decomposition with the interfaces properly policed.

Functional decomposition is, of course, a long established process of program design and ADA fits in well with this process. However, functional decomposition is not always appropriate• It relates to analysis of the program according to its flow of control but says nothing about the analysis of the data. Newer design techniques consider the data as the dominant entity and one which has recently gained much popularity is the so called 'object oriented' design approach. In this we consider the objects with which the program is concerned, the operations performed on the objects and their visibility. An important issue is to hide the inner details of an object and only provide an abstract view of it much as the package body hides the inner details of the workings of the stack. One of the major strengths of ADA is that it has facilities for doing just this.

In languages such as PASCAL we are used to giving a name to a type declaration such as

type PERSON is record

AGE:INTEGER; SEX:GENDER;

end record;

We can then declare and manipulate objects of the type PERSON but naturally enough the fact that the type is implemented as a record and has certain components is quite visible• However, in ADA we can use a private type, something like

package COMMUNITY is --various subprograms acting on --the type PERSON

type PERSON is private; • . . - - various subprograms acting on ... - - the type PERSON

private type PERSON is record

end record; end COMMUNITY;

From the point of view of the user all that is known is the existence of the type PERSON and certain procedures and functions which operate upon objects of that type. The fact is that the type is actually a record and the details of its inner construction are now quite hidden from the user. The type is thus treated as an abstraction. This is important for program maintenance and means that we can change our mind about the detailed construction of the type, secure in the knowledge that the rest of the program will not be upset.

To summarize, ADA is technically important because it offers facilities that are in line with software engineering. It offers separate compilation, abstraction and hiding properties that neatly implement the twin methodologies of functional decomposition and object oriented design.

Vol 11 No 5 June 1987 247

Page 4: Introduction to ADA and its use for embedded applications

This section has illustrated some major facilities of ADA. Realtime aspects of the language are considered below. However, there are a number of other important properties which there is not space to describe in detail but which deserve a brief mention.

ADA contains a means of writing generic software, i.e. software that can be tailored for particular applications without loss of execution efficiency. This should enable the reusable software to be written for a wider range of applications than in the past.

ADA is also, for some rather ill defined reasons, very readable. Perhaps it is the bracketed control structures which are so much better than those of ALGOL, CORAL and PASCAL. Perhaps it is the encouragement of long identifiers (compilers must accept identifiers of any length). Perhaps it is the localization of declarations, constants, array and record operations, the ability to initialize objects neatly, and so on. In the event, those who have used ADA claim that although it is perhaps a bit wordy to write in the first place, it is much easier to read and understand than other languages.

Finally, strong typing must be mentioned. The ability to declare user oriented data types is familiar to those who have used PASCAL but not to those who use only ALGOL, CORAL or FORTRAN. Properly used, strong typing (with constants and ranges as illustrated above) enable the programmer to express directly in the program various properties of the objects concerned. They do not remain merely in the mind of the programmer or i~noncheckable documentation. The compiler is able to use the infor- mation provided to ensure that the program does in fact use the objects correctly.

The net result of all these abstraction and other checkable properties of ADA is that the compiler is very sensitive to imperfect programs, and it is indeed quite hard to get a program to clean-compile. However, having got past the compiler, experience shows that programs are very nearly correct and very little debugging remains to be done at execution. This is a great advantage. It means that more error detection is being done in a controlled (almost automatic) manner by the compiler and the difficult and unpredictable process of dynamic debugging is much reduced compared with earlier languages.

EMBEDDED SYSTEM REQUIREMENTS

Realtime embedded system applications have a number of special requirements which can make severe demands on the programming language and its associated tools. These include

• Good-quality (fast) object code. • Ability to respond to external asynchronous events. • Ability to access and control special I/O devices. • Access to a realtime clock. • Ability to recover from runtime errors. • Ability to handle parallel processes.

This last item can be subdivided into a number of details, but this subdivision depends to some extent on the philosophy of the language system. We must take care to distinguish the real requirements from the details of the technology chosen to meet them. Thus we may or may not agree on the need for hierarchies of processes, but we can probably agree that major user issues are the ability

• to send messages from one process to another • to control access to shared data

Below, these various areas are considered in turn, ~tartin~,. with the functional concepts of parallel processin~

Parallel processes

In ADA, parallel processes are known as tasks. ADA distinguishes between the static program text describing a template for a parallel activity and the dynamic activation of such text. Furthermore, in a similar way to the package there is a clear separation between a task specification (which describes the interface that a task presents to other tasks) and a task body (which describes what a task actually does).

In the general case, we write the template as

task type T is --specification

end T;

task body T is --body

end T;

The task specification describes the interface as the declaration of a number of 'entries'. These act as 'ports' to other tasks (see below). It is often the case that a task provides no formal interface to other tasks in which case the specification can be written simply as

task type T;

The task body describes the statements that will be obeyed when the task executes. In a similar manner to other ADA program units it also contains declarations, so in more detail

task body T is --some declarations

begin --some statements

end T;

It is important to realize that the above declarations do not result in any parallel activites; they merely describe potential activities in much the same way as a data type definition describes potential objects. Thus, writing

type ANIMAL is (CAT, DOG, RABBIT);

does not create objects. To do that we have to declare something like

GEORGE:ANIMAL;

In a quite analagous manner we create an actual dynamic task by declaring a task object thus

DRIVER:T;

This declaration, when obeyed (or elaborated as ADA- speak says), brings into existence a parallel activity with an associated task control block which we can refer to as DRIVER. Several identical tasks of the type T could be declared by a series of declarations

DRIVER_l, DRIVER__2, DRIVER__3 :T;

or even an array of such things

DRIVERS:array (1 .. 10) of T;

which could then be referred to by index in the usual way.

248 Microprocessors and Microsystems

Page 5: Introduction to ADA and its use for embedded applications

Although it is sometimes necessary to have multiple similar tasks, especially when modelling the external world, nevertheless there are many cases where clearly only one is required and the overhead of first declaring a task type then becomes a burden. In such cases, as a shorthand, we can write

task DRIVER is

end;

task body DRIVER is

end DRIVER;

in which the word type is omitted from the specification. Most of the examples will take this form.

Activation and termination

There are two main approaches to creating parallel processes. Some language systems treat all processes as permanently in existence - - in some way created by the program builder and perhaps all set active on program initiation. This is the flat approach. It is simple-minded, has a fixed number of tasks in a given system and consequently has relatively easily determinable storage requirements. The other approach is hierarchical, in which processes are created dynamically during program execution. The number of processes is, in general, not known prior to execution and consequently the storage requirements are less certain.

ADA follows the second model. ADA tasks are treated like other objects and are created by being declared in a block or other program unit. This containing unit can be referred to as the 'parent'. Such a newly created task becomes active when the parent passes the 'begin' following the declarations. There is a corresponding rule regarding termination. A parent unit cannot be left until all locally declared tasks are terminated; a task normally becomes terminated by running into its final end. Thus the following general scheme is obtained:

declare

DRIVER:T;

begin

° , .

end;

--start of block

--task created here

--set active here

--tasks running in parallel

--parent awaits local task termination

ADA thus exhibits a general 'fork and join' policy. The detailed rules regarding activation are somewhat complex because of possible interaction with exceptions and so will not be described in detail. The rule regarding termination is necessary in order to avoid problems of access to variables declared in the parent and accessed directly by local tasks. Such shared access is permitted in ADA and, of course, without the termination rule there would be difficult problems of scope control.

This organization clearly allows considerable flexibility in task creation. However, this is still tied to the block structure and so is somewhat constrained. Through the

use of access types (the ADA term for pointer types), it is possible to create tasks in a completely fluid manner:

type POINTER is access T; P:POINTER;

and then P is an object which points at tasks of the type T. We can create such a task and point to it by the use of the allocation mechanism, thus

P:= new T;

This is a statement and so can be executed in a general manner. The activation and termination rules are now modified: the task becomes active on allocation (strictly, when the parent reaches the semicolon) and must terminate before the parent can leave the scope of the access type declaration.

Task communication

Tasks can communicate with each other by two means: by using shared variables (frowned upon by the theorists), and by the use of entries. An entry belongs to a task and is declared in its specification. It has parameters in much the same way as a procedure and is called by another task in much the same way that a procedure is called. Thus we might have

task T is entry E(I:INTEGER);

end T;

where the entry has a single parameter of type INTEGER. This entry could be called from another task by a statement such as

T.E(38);

in which the called task T is followed by a dot, the entry name and then the actual parameters, in this case 38, say. Within the body of the task T there will be an accept statement for the entry E, thus

task body T is

begin

accept E(I:INTEGER) do

end E;

end T;

The 'do' and 'end' of the accept statement surround a sequence of statements that are obeyed by the task T in response to some other task calling the entry E. However, the accept statement can only be obeyed when the flow of control of the task T runs into the statement in the usual way. Until this happens any task calling the entry is suspended. Similarly, if the task T encounters the accept statement before the entry is called then it is suspended until some other task does call it.

In the general case there will be a queue of tasks waiting to call the entry E. Each time T obeys the accept statement, it takes one call from the queue, the parameters are then passed into the accept statement which is then obeyed; the calling task remains suspended until the accept statement is completed, then any out parameters

Vol 11 No 5 June 1987 249

Page 6: Introduction to ADA and its use for embedded applications

are updated and then the two tasks can continue independently. This whole mechanism is known as an 'extended rendezvous': the term 'rendezvous' expresses the concept that the two tasks must meet for the transaction to take place; the qualification 'extended' reflects the fact that a quite general amount of computa- tion can occur during the rendezvous.

The asymmetry of the ADA rendezvous should be noticed. The calling task names the called task but not vice versa. Thus there is a server-user relationship between the tasks. This facilitates the writing of general- purpose library routines. As a simple illustration consider the following task which implements a simple one- element buffer between producer tasks and consumer tasks:

task BUFFERING is entry PUT(X: in ITEM); entry GET(X:out ITEM);

end;

task body BUFFERING is V:ITEM;

begin loop

accept PUT(X:in ITEM) do V:= X;

end PUT; accept GET(X:out ITEM) do

X:= V; end GET;

end loop; end BUFFERING;

Other tasks may then dispose of or acquire items by calling

BUFFERING.PUT(...); BU FFERI NG.G ET(...);

Intermediate storage for the items is the variable V. The body of the task is an endless loop which alternatively accepts calls of PUT and GET. The protocol is thus enforced by the static structure of the task.

The accept statement provides the basic means of task communication, but alone it is too rigid. Some means of choosing between a number of possible actions is needed. In ADA, this can be done by the select statement. Consider the problem of protecting a variable V from uncontrolled access. We wish to allow reading and writing to occur in any order but without interference. This can be done as follows:

task PROTECTEDVARIABLE is entry READ(X:out ITEM); entry WRITE(X: in ITEM);

end;

task body PROTECTED_VARIABLE is V: ITEM:= --some initial value

begin loop

select accept READ(X:out ITEM) do

X:= V; end;

o r

accept WRITE(X:in ITEM) do V:= X;

end; end select;

end loop; end PROTECTED VARIABLE;

As above, the task body consists of an endless loop which contains a single select statement. The simple form of the select statement shown here consists of a number o~ branches separated by 'or'. In this example each branch contains just an accept statement, one for READ and one for WRITE. When the task encounters the select statement various possibilities have to be considered according to the calls of the entries outstanding. If neither entry has been called, then the task waits until one is called and then takes the corresponding branch and obeys the accept statement therein; if only one entry has been called then, naturally, the task takes the branch for that entry and deals with one call. If, however, both entries have outstanding calls, then an arbitrary choice is made between the two branches.

The net effect is that each time round the loop the task deals with one entry call and will deal with whatever is available. Protection is assured because it is the control task that does the accessing and it can, of course, only do one thing at a time. However, the select statement does not impose a protocol of access (unlike the previous example) and so arbitrary access is allowed.

The final example illustrates a more general form of a select statement with guards. It consists of the classical bounded buffer with N internal locations:

task BUFFERING is entry PUT(X:in ITEM); entry GET(X: out ITEM);

end;

task body BUFFERING is N:constant:= 8; - - for example A:array (1 .. N) of ITEM; I, J:INTEGER range 1 .. N:= 1; COUNT:INTEGER range 0.. N:= 0;

begin loop

select when COUNT < N => accept PUT(X:in ITEM) do

A(I):= X; end; I:= I mod N +1; COUNT:= COUNT + 1;

or when COUNT > 0 => accept GET(X:out ITEM) do

X:= A(J); end; J:=J mod N + 1; COUNT:= COUNT- 1;

end select; end loop;

end BUFFERING;

In this case each branch of the select statement starts with a guarding condition. Execution of the select statement commences by evaluating all the guarding conditions. It then proceeds as before but only those branches whose guards are TRUE are considered on that occasion, so PUT can never be accepted when the buffer is full and GET can never be accepted when the buffer is empty. Note that each branch contains some statements after the accept statement, and these are obeyed as part of the branch but outside the rendezvous. They can be thought of as housekeeping operations to be performed as part of the service but for which the presence of the customer is no longer needed. In understanding this more general form of select statement it is very important to realize that the guarding conditions are re-evaluated each time the select statement is encountered.

250 Microprocessors and Microsystems

Page 7: Introduction to ADA and its use for embedded applications

Just described above is the general standard form of select statement. There are other forms that allow for conditional and t imed-out accept statements and also conditional and t imed-out entry calls.

At this point the ability of ADA tO meet the two fundamental requirements mentioned ear l ie r - - to pass messages and to protect access to shared data (critical reg ions ) - is considered. It is clear that the entry call provides a means of passing messages directly and that the use of slave control tasks such as BUFFERING gives protected access to shared data. Thus, in a general sense ADA provides the necessary functionality. As further evidence, it is possible to simulate semaphores in ADA and so anything can be done, but naturally this will usually be a foolish way to proceed.

Experience seems to show that the ADA high-level approach works well most of the time but that there are a small number of cases where it gets badly bogged down. These mostly concern prioritized resource allocation where we really want to dabble with the entry queues directly.

Another possible area of weakness concerns task identities. It is not possible to directly ask a task who it is, and in any case there are no generic task variables and so nothing could be done with such information anyway. Various mechanisms can be used to overcome this: creating and passing tags corresponding to individual tasks and perhaps using agent tasks as intermediaries.

In defence of the ADA approach it might be argued that the tasking was designed for cooperating processes and not for writing operating systems where the user tasks are by definition uncooperative. Experience to date has shown that ADA tasking is marvellous for simulation purposes.

One criticism often heard is that ADA demands many slave tasks compared with other language systems and that this degrades performance. This is probably the case in some instances. The trick is to use the right facilities for the appropriate purpose. If you are in a hurry then the use of shared variables may be the only way. Although these are generally frowned upon, ADA recognizes their value and provides a vague pragma which is meant to stop the compiler doing unhelpful optimizations with shared variables.

A general conclusion might be that the ADA rendezvous is excellent when it works, is easy to use, understand and get right; it is a high-level approach. It contrasts vividly with raw semaphores which are difficult to use and very difficult indeed to debug.

Code efficiency

A program that is too slow to meet realtime demands is quite useless. Unlike other areas where a slow program might be just inconvenient and expensive to use, in this area it is no good at all. If a high-level language system generates poor code then there will be a temptation to use a large number of machine-level insertions and these will destroy the clarity and maintainability which was looked for in the first place.

Aside from the tasking, ADA is a simple language at the object code level. The complexity of the language is in analysing what the program mer said and not in the actions to be done. Thus an ADA compiler should be able to generate code every bit as well as PASCAL or c. Of course many early compilers have generated poor code but the situation has changed a lot over the last year. There are a

number of reasons for the poor code of early compilers: haste in getting something to the marketplace; absence of any quality tests from the DoD; and perhaps the daunting problem of getting to grips with ADA in a way which enables compilers to be both portable and efficient.

The rendezvous and task switching is perhaps another matter. Reported rendezvous performance is disappoin- ting in most cases and this needs to be improved. One problem is undoubtedly that most existing object systems run on top of some cumbersome executive such as Unix. Interaction with the exception mechanism may be another cause in some instances.

Responding to external events

This is a fundamental requirement. There needs to be some way of linking the occurrence of an external interrupt to statements in the language. ADA prescribes a mechanism whereby the external event is seen as an entry call by some hypothetical external task. An 'address' clause is used to indicate this linkage although much of the interpretation is necessarily machine and system dependent. We could write

task CONTACT__HANDLER is entry CONTACT; for CONTACT use at 8#72#;

end;

task body CONTACT_HANDLER is begin

loop accept CONTACT do

end; end loop;

end CONTACT__HANDLER;

At the time of writing it is not known whether this mechanism has been used in practice. Few compilers implement such fringe features.

Another approach might simply be to treat the interrupt response as the execution of some prescribed subprogram. The start address of that subprogram could then be linked to the interrupt vector in some way, again using an address clause:

procedure CONTACT is

end CONTACT; for CONTACT use at . . . ;

ADA provides enough hooks to enable interrupt responses to be linked up but no consensus seems to have been reached as yet regarding the best method in practice.

Access to special I / 0 devices

There are two ways of accessing special devices. The most obvious is through the standard package LOW_LEVEL__IO. This contains various overloadings of two procedures SENDCONTROL and RECEIVE_CONTROL, thus package LOW__LEVEL__IO is

- - declarations of types for DEVICE and DATA - - declarations of the procedures: procedure SEND__CONTROL (DEVICE:device_type;

DATA :in out datLtype); procedure RECEIVE_CONTROL(DEVICE:device_type;

DATA :in out datLtype); end LOW__LEVEL__IO;

Vol 11 No 5 June 1987 251

Page 8: Introduction to ADA and its use for embedded applications

The actual behaviour of these procedures and the definition of the two types clearly depends on the implementation.

One common way for data to arrive in a realtime application is through direct memory access, i.e. it is placed in memory autonomously by some external agent outside the immediate control of the program. To access such data the program merely needs to read the appropriate address. This is quite straightforward in machine languages but the abstraction mechanisms of high-level languages provide a barrier that needs to be overcome in a formal manner. Junk implementations of BASic-like languages often provide procedures PEEK and POKE which take a numeric address as a parameter and then allow the user to dabble at will. A better approach, and that used by ADA, is to somehow declare an appropriate variable and to ask the compiler to map it onto the appropriate address.

Taking a simple example, suppose some integer value is being sent to location octal 1234 and we wish to read it by simply accessing an integer variable COUNTER. This can be done by a further use of an address clause thus

COUNTER: INTEGER; for COUNTER use at 8#1234#;

However, another possibility is that the incoming infor- matior~ consists of some status information with different single bits set according to the status: bit 1 for OFF, bit 2 for READY and bit 3 for ON, perhaps. We could, alternatively, view the input as a byte with three possible values: 1 for OFF, 2 for READY and 4 for ON. Within our program, however, we would probably like to treat these values as an enumeration type

type STATUS is (OFF, READY, ON);

One possible approach is to map a variable of the type STATUS at the correct location. In addition the compiler must be instructed to map the incoming values onto the required enumeration literals; by default, the compiler would probably take 0 as off, 1 as READY and 2 as ON. The following will suffice:

type STATUS is (OFF, READY, ON); for STATUS use (OFF => 1, READY => 2, ON => 4); S :STATUS; for S use at...

The trouble with this approach is that it does not allow us to check that the incoming value is one of the legitimate set. If an illegitimate value turns up, then our program will ramble on with a meaningless value in the variable S.

A better approach is to treat the incoming value as a numeric value, say a type BYTE, check it and then convert it to the type STATUS by a direct map. This final mapping naturally breaks the typing rules but can be done by a predefined generic function provided for just such low-level programming. We first need to instantiate this generic function for our mapping:

function BYTE__TO__STATUS is new UNCH ECKED_CONVERSION (BYTE, STATUS);

We can now write

S: STATUS B: BYTE; for B use at... ;

case B is when 1 d214 => null; when others=>raise DUD VALUE:

end case;

S:= BYTE_TO STATUS(B);

It must be emphasized that the mapping function actually does nothing at all so no cost is incurred at run time; it is merely a way of informing the compiler of our change of view of the value. Note also how an exception is raised when something goes wrong. This causes control to pass to an exception handler where the exceptional situation can be processed. Of course some other action could have been programmed at the point of detection.

ADA therefore contains a number of facilities aimed at low-level programming and which provide good facilities for manipulating the fine detail when necessary. Again, however, the facilities are somewhat implementation dependent and may not be found in all compilers.

Access to clock

A realtime program needs access to the clock and some ability to synchronize internal events with external time. Some languages (such as PEARL) have attempted to provide high-level facilities in this area, such as the ability to schedule a particular activity at regular intervals. This approach does not seem to have been particularly successful for some reason and ADA takes a fairly simple and traditional approach. The statement

delay some_expression;

suspends the task issuing the call for at least the interval given ('at least' because a higher priority task may have the processor at the expiry of the interval). The expression is of a fixed-point type DURATION and the unit is a second. Fixed point is chosen because of its accuracy in addition and its ability to express fractions. Using a unit of a second is not a problem given the ability to declare constants. Thus

M IN UTES : constant: = 60; HOURS :constant:= 3600; SECONDS: constant:= 1;

and then

delay 2*HOURS + 30*MINUTES;

There is also a predefined package CALENDAR which provides access to the realtime clock in terms of a type TIME and various operations on times and durations:

package CALENDAR is type TIME is private:

function CLOCK return TIME;

function "+" (LEFT: TIME; RIGHT: DURATION) return TIME; function "+" (LEFT: DURATION; RIGHT: TIME) return TIME; function "-"(LEFT: TIME; RIGHT: DURATION) return TIME; function " - " (LEFT: TIME; RIGHT: TIME) return DURATION;

end;

252 Microprocessors and Microsystems

Page 9: Introduction to ADA and its use for embedded applications

A value of the private type TIME is a combined time and date. Various subprograms are also provided to enable times to be built out of and decomposed into the constituent parts: year, month, day and duration since midnight. Note the distinction between the types TIME and DURATION; TIME is absolute and DURATION is relative.

A common problem is repeating some action at (nearly) regular intervals. This is often difficult because of conflicting demands on the processors. A simple loop such as

loop delay 5*MINUTES; ACTION;

end loop;

is not adequate because there is no guarantee that the task will regain the processor at the expiry of the delay for, as mentioned above, some other task of higher priority may be active. Also, we have not allowed for the overheads of the loop and the call of ACTION itself. A good approach which avoids cumulative drift is shown by the following which uses the package CALENDAR:

declare use CALENDAR; INTERVAL:constant DURATION:= 5*MINUTES; NEXT TIME:TIME:= FIRST_TIME;

begin loop

delay NEXT_TIME - CLOCK; ACTION; NEXT_TIME:= NEXT_TIME + INTERVAL;

end loop; end;

ADA facilities in this area, although low level, seem to be satisfactory.

Error recovery

ADA contains a mechanism for signalling and recovering from errors based on the concept of exceptions. A brief example was used above when checking the incoming status value.

An exception is an event which occurs when something goes wrong at run time. Some exceptions are predefined and correspond to the violation of various language rules. An example is TASKING_ERROR which is raised (i.e. it occurs) if something goes wrong with an entry call, such as the called task having already terminated. The user may also introduce his/her own exceptions by declaring them

DUD VALUE:exception;

An exception occurs either as the result of an inbuilt check in the language system or as the result of a raise statement:

raise DUD VALUE;

When an exception occurs control passes to an 'exception handler' for the particular exception. These handlers can be placed at the end of any block, subprogram, task or package. The program in fact unwinds up the dynamic chain of activation until an appropriate handler is found or the main program is reached, in which case it terminates, hopefully with some helpful message from the language runtime system. A block with handler has the following outline:

declare

begin

exception

end;

--declarations

--statements

when DUD_VALUE = > --statements to be obeyed as --the recovery action from a --DUD VALUE

when TASKING__ERROR = > --statements for TASKING_ERROR

An important point is that control never goes back to the point of error, a technique that rarely works. The statements of the handler effectively replace the unit concerned.

The ADA exception mechanism works reasonably well. A criticism is that little information is available in the handler to explain the cause of the particular failure. The strong typing makes the provision of such information difficult in general. A reasonable approach is for the user to set auxiliary information in appropriate state variables which can then be interrogated.

Other matters

A number of issues have not so far been covered. One is scheduling. ADA models each abstract task to have its own processor, and any scheduling is an implementation matter. However, there is a mechanism of preemptive priorities which is useful for humble hardware.

Another matter in the area of errors is the rather violent abort statement which enables one task to annihilate another. This must be used with caution since it can have interesting effects, for example if the doomed task is engaged in a rendezvous with an innocent task.

Many of the tasks in the above examples were endless loops. This means that we cannot leave the scope declaring them. This is a nuisance and although we could program a special entry call to tell such tasks to stop themselves, there is also a neat method using the 'terminate' alternative in a select statement.

IMPLEMENTATION A N D EXPERIENCE

Although ADA was designed for the realtime area it so happens that most applications to date have been of a more general nature. This is largely because of the characteristics of most ADA compilers that have been available until recently. A major reason for this is undoubtedly the introduction of the validation mechanism by the US DoD. To comply with US trademarking, a vendor can only call an ADA compiler as such if it has a validation certificate, which is obtained by showing that the compiler behaves satisfactorily with a certain set of test programs. Vendors have, quite naturally, set their initial objectives at gaining this coveted certificate above all else.

In the long term the validation mechanism is an excellent idea and will promote the uniformity of implementation that has been missing in so many other languages. However, in the short term the test suite has not been comprehensive and so vendors have not been

Vol 17 No 5 June 1987 253

Page 10: Introduction to ADA and its use for embedded applications

obliged to implement certain fringe features of the language. These features include the critical representation clauses (such as were used for directing the mapping of the status input) which are vital for close communication between the program and its hardware environment.

The situation is now changing, however, as later versions of the validation suite become more demanding. This will force the vendors to produce systems more in tune with the needs of the embedded system sector.

Another interesting point concerns the background and enthusiasm of much of the ADA development community. They are largely software engineers with a concern for the subject as an emerging science and for ADA as a modern general-purpose language that can supplant the horrors in use in all areas. They have not had the 'nitty-gritty' background of, for example, the end users in the UK who developed parochial languages such as CORAL and RTL/2, and so have not been fully aware of the special needs of embedded systems.

These facts probably explain why the first implementa- tions of ADA have treated it as a general-purpose language and so have initially attracted users from diverse areas such as numerical analysis, data processing and simulation rather than embedded systems.

In conclusion, apart from a few odd corners which have been mentioned, ADA as a language is eminently suitable for embedded systems. Implementations are steadily being improved and are now becoming adequate for this demanding area.

REFERENCES

1 Department of Defense requirements for high order

computer programming languages-- ~teelmar~ U:~ Defense Advanced Research Projects Agencv~ Arlington, VA, USA (June 1987)

BIBLIOGRAPHY

Barnes, I G P Programming in ADA Addison Wesley, Wokingham, U K (1983) Burns, A Concurrent programming in ADA Cambridge University Press, Cambridge, U K (1986) Booch, G Software engineering with ADA Benjamin Cummings (1987)

John Barnes read mathe- matics at Trinity College, Cambridge University, UK. He then joined Imperial Chemical Industries where he designed and imple- mented the RTL/2 language for process control and allied applications. He is now managing director of Alsys Ltd, which is part of the Alsys Group and is developing a

number Of ADA compilers and other ADA products. John Barnes has been involved with ADA for a number of years, first as advisor to the UK Department of Industry and then as a member of the Ada Language Design Team. He also directed the UK consortium, Ada Group Ltd, in its efforts to design and implement an ADA programming support environment. He has given many courses and lectures on ADA and is the author of two books.

254 Microprocessors and Microsystems