Tom Schlintz has a BSEE from Virginia Polytechnic Institute and has been working as a software engineer for Logical Design group for the past four years. His Interests are embedded and VME bus-based applications. Greg Goslen has a BA in Physics from Appalachian State University and has been working as a software engineer for Cardiovascular Diagnostics for the past year. He has nine years of embedded systems experience, four of them working in C. Bill Bingham has a BA in Biology from The University of Virginia, an MSEE from Berkeley, and MEng Bioengineering from Berkeley. He is currently the senior research and development engineer at Cardiovascular Diagnostics, where he has worked for the past two years.
When most programmers think of object-oriented programming (OOP), they think of object-oriented languages. What we often forget is that OOP is more than just using a type of compiler. It is a programming concept a way of thinking about the way we structure our code.
We're writing this paper to demonstrate three things: first, that OOP isn't as scary as you may have been led to believe; second, that no one person, group, or manufacturer has an exclusive on the implementation of OOP; and third, how we addressed a small embedded systems project, using an OOP approach, and made it work.
Several months ago we were struggling with the code for a new medical device. We required that the code be clear, modular, and thoroughly testable, an ideal candidate for object-oriented programming techniques. Unfortunately, it also had to perform a large number of tests in limited ROM (48KB), limited RAM (8KB), and on a processor built for small, embedded applications, the 68HC11. To further complicate matters there was neither an OOP compiler for our platform nor the code space to support a full-blown OOP strategy. Still, we wondered if we could use a standard, high level language to get some of the benefits of OOP without sacrificing too much space.
Once we began this project, the first thing we noticed was that we already knew OOP, we just didn't know that we knew. The concepts are not difficult, we had all used some of them before. The major difference is that in an OOP language the constructs are supported by the compiler.
Implementation
The classic object-oriented system creates objects in RAM, allows them to communicate with one another and selectively destroys them once they have finished their tasks. In an embedded system, RAM is precious, so we created object classes manually using typedefed structs, which we stored, along with object definitions and pointers to object methods, in ROM. In essence we did manually what an OOP compiler does for you.To further conserve ROM space we used the concept of inheritance to eliminate redundancies between tests. We began by creating a parent class, TEST_CLASS as a typedefed struct, to be the master template for our test objects. It is defined in the header file class.h, as shown in Listing 1, and is fixed at compile time. Listing 2 shows how, by creating variables of type TEST_CLASS, several objects, test_a, test_b, and test_c, inherit their basic structure from it.
To further conserve ROM we created several subclasses of TEST_CLASS. We placed class specific methods and data into a common module and object specific methods and data into object specific modules. Objects of a given subclass inherit data and methods from that subclass.
Thus, test_a and test_b are of the same subclass, class 1, and both use class_1_display, which is defined in Listing 3, class_1.c. This is an example of inheritance from a subclass. test_c is of a different subclass, class 2, and so inherits class_2_display. All the test objects define their own public data, e.g., Test_Name, and public methods, e.g., init_object.
Using our protocol the names of all methods are fixed when a class is created. When a subclass inherits method names from the parent class we manually attach one of several functions to the pointer associated with that method name. By using this form of polymorphism we reduce the complexity of the main program and force implementation specific details to remain encapsulated within the objects.
Our project specified twenty different tests, and that adding, changing, or deleting a test be easy and have no impact on existing tests. To achieve this we created a master array of pointers, test_ptr[], in the main program file. The elements of the master array point to the structure defining each test object. An index into test_ptr[] yields a pointer to the selected test object (see Listing 4) . The selected object then performs the test via its methods. In order to compile properly, test_a, test_b, and test_c must be declared as externals above the declaration of test_ptr[]. The last, null entry in test_ptr[] is used as a termination to allow any number of tests to be incorporated. The Test_Name[] entry in every object of type TEST_STR allows the main program to search for the correct object by matching Test_Name to a string.
The invoking program doesn't need to know which routine it is using, only that, for example, it is calling a display method. The display method handles the details. This makes it easy to add tests. It may seem like this is a lot of work to go through, when it would be much simpler to completely define test objects as structures throughout the program. If, however, we wish to add a new test with new functionality, this approach allows us to create new methods without changing data flow.
Since we were already using separate modules to implement inheritance we decided we could go further and use the C specification to implement information hiding. C forces knowledge of variables declared at the beginning of a file to be confined to the routines in that file. This allowed us to define static variables at the top of the file that are freely accessible to all routines within the file, while hiding these variables from routines outside. Outside access to the data can be tightly controlled by routines within the file. We used this aspect of C to treat the contents of a file as an object, and the routines and data within the file as methods and private data of that object. By defining an object as files only the methods defined in that file have access to the object data.
We recognized that data hiding necessitated some form of messaging. What we needed was a flexible and not too test specific, messaging scheme. Each test expected a different set of inputs and produced a different set of results. If these parameters were declared explicitly in the test method argument list, each test would have had to be a separate class. On the other hand, if we had used TEST_CLASS to define all of the tests, all instances of a given method would have had identical argument lists.
Our solution was to establish global, generic floating-point arrays that could be passed as parameters to any method of the current test object (see Listing 5) . In our case all data could be passed as floating point but a more generic interface could be constructed using an array of strings. Structures were typedefed at the top of each object file which described the format used by all the methods of that object. The objects used the structure to encode or decode the information stored in the arrays.
The parameter array is allocated in global RAM and is bigger than the maximum number of parameters for any test. A similar array of floats is defined for passing results back from tests. The main program passes the pointers to the params and results arrays to the methods when it invokes them and never knows what is in these arrays. Its job is to coordinate the passing of the arrays from one method to the next. The arrays combined with the object specific template act like a secret decoder ring available only to the methods within the object and allowed us to use meaningful names instead of index numbers to refer to each parameter. The result was much clearer code.
Listing 6 shows a test called test_a which requires that quantities called time and amplitude be passed to it. Notice that the parameter template maps the first two elements of params to these names. This template resides in the class specific module file (class_1.h shown in Listing 6) .
The methods shown in Listing 3 take these pointers and cast them with the same class specific template into a pointer to a structure of type PARAMETER_STR. In Listing 6 the method casts the PARAMETER_STR to the method specific pointer, ps. The method can now interpret the parameter string values according to its own definition and can refer to any of the parameters by name, as shown in Listing 6. A similar mechanism is used to place results in the results array.
Summary
The effort we expended to implement OOP in our system has been paid back by the ease of adding new tests to the system, the disappearance of cross-test interference, and greatly decreased debugging time. Once implemented, OOP techniques helped us to achieve a higher level of abstraction. Because implementation details were left up to the private methods within each of our objects, our code could ignore how things were implemented on lower levels. This freed us to concentrate on the design aspects of the problem.We would like to thank Murdock Taylor for developing the hardware platform we used in this project.
Sidebar: "Basic OOP Terms"