ONLamp.com
oreilly.comSafari Books Online.Conferences.

advertisement


Black Box with a View, Part 2
Pages: 1, 2, 3, 4, 5, 6

Object-Oriented Design for Microcontrollers

The layered Hello World program from the previous section has some object-oriented characteristics. It begins by creating and initializing an LED object. Then, you use the object's methods to control the LED. To keep the discussion clear, Example 1 defines just one such method--LED_Toggle. Note that the constructor (LED_Init, also defined in Example 1) is really a method of the class, not the object. You call the constructor to bring an object into being; it makes no sense to use it on an object that already exists.



Of course, the C compiler cannot enforce such distinctions--it lacks the object-oriented programming support of languages such as C++ and Java. The programmer must therefore add the required features herself, as a mixture of code and a set of coding rules that she then must follow manually. During this process, it is important to keep in mind that the programs will run in the tightly constrained microcontroller environment.

In Example 1, a simple C struct (called _LED) stores the LED object's state. You must pass a pointer to a struct _LED as the first argument to every method, including the constructor. You therefore create the object in two steps--see the section Avoiding the Heap for the reasons. Refrain from calling the constructor again on the created object.

The constructor takes the port and bit mask (see Appendix B for more information on bitwise operations) of the LED as arguments. After you create the LED object, you should never handle the LED hardware directly, but only through the object's methods. The LED object now "owns" the hardware resource.

Note, however, that port 2 is still configured (P2DIR=0xFF) in the main program, not in the LED object. In fact, the LED object does not own the entire port. In a larger application, other devices could be attached to the remaining pins of the I/O port. You would then transfer the ownership of these I/O lines to the appropriate device drivers. If you are new to microcontroller programming, you may want to review Appendix B for more information on ports, pins, and I/O lines.

In general, the concept of resource ownership is a very important tool in software design. For example, to effectively manage the memory in a C++ program, you must be consistent about which part of the program owns each piece of allocated memory.

One important benefit of having the LED object own the LED hardware is encapsulation. The object hides the details of hardware access from the rest of the program, and provides a consistent interface to control the device. You can change the _LED structure, port the program to a different microcontroller, alter the implementation of LED_Toggle to keep statistics on the system state, and so on, without breaking any client code that uses LED objects.

Unfortunately, the C language does not enforce these safeguards. You must voluntarily refrain from accessing the _LED structure directly. It is therefore very important that you formulate consistent rules during the design phase of your project, in order to avoid programming errors that could break the encapsulation.

For example, choose one way of creating any kind of object, even if this method is not always the best. Do not optimize your program prematurely--exceptions to the rules should be very rare. Strive for a uniform way of coding early, and you will avoid many days (or even weeks) of frustrating debugging later.

If you are familiar with object-oriented programming, you have probably noticed that something is missing--namely inheritance. Many common definitions of an object-oriented language still emphasize inheritance, while the approach taken in this article (which avoids inheritance entirely) is sometimes referred to as object based.

Inheritance, however, has become an overused--even harmful--feature in object-oriented languages, as described in the classic book Design Patterns. The best design emphasizes very shallow hierarchies, as shown in Figure 1.

Thumbnail, click for full-size image.
Figure 1. Inheritance strategies--click for full-size image

The goal is to create a set of objects with well-defined interfaces. Then, it is possible to build the a program from these objects--just like connecting together LEGO blocks. This is composition; in practice, it leads to much greater code reuse than inheritance. The Hello World, Interrupted section of the article illustrates composition.

Of course, some object-oriented languages (such as C++) use inheritance to define interfaces. In such cases, the base class at the top of the hierarchy is abstract. It cannot be instantiated directly, and servers only as an interface specification to its child classes, which can be instantiated.

The right side of Figure 1 illustrates precisely this kind of inheritance--the abstract base class is red. With hierarchies rarely exceeding one level in depth, however, the techniques presented in this article can be used to define interfaces without any formal inheritance mechanism.

The next article will extend these techniques to create a whole set of classes, all of which will support the same interface. This is polymorphism--the most important concept in object-oriented programming. It will also be central to the implementation of a connected embedded system.

Avoiding the Heap

As Example 2 demonstrated, it takes two steps to create an LED object. First, define a variable to hold the object state. Next, initialize the object with a call to LED_Init. The code in Example 1 includes a typedef to use for the first step. The typedef actually creates a single-element array of struct _LED. Just use the array name as the first argument to every method of the LED object, as well as the constructor. The C compiler will convert the array name to a pointer automatically.

This two-step object creation technique is a means to avoid dynamic memory allocation from the heap. On an ordinary PC, the LED constructor can be a function that allocates memory for the struct _LED, initializes the structure, and returns a pointer to it. The article Object-oriented programming in C contains an example of such a constructor.

In microcontroller development, however, you should generally avoid dynamic allocation from the heap, for the following reasons:

  • Microcontrollers usually run a single application that performs a narrow set of tasks. Dynamic allocation and release of memory is really a means to share this resource between multiple unrelated tasks. It rarely makes sense in microcontroller environments.
  • Microcontrollers have little memory compared with other platforms (such as a PC or even an embedded Linux system). It is usually easier and more efficient to manage this small amount of memory without treating it as a heap.
  • Embedded software must often perform critical tasks. Failure during normal operations (such as due to an unsuccessful dynamic memory allocation) is not acceptable. For example, it is much better for an aircraft engine to fail to start on the ground than to stop running suddenly during flight.

The last point is particularly important. Embedded programs are simpler, more efficient, and safer when they do not rely on unpredictable heap allocation.

Of course, you can also avoid the two-step object creation process by defining LED_Init as follows:

#define LED_Init(r,m) {{(r), (m)}}

Then, create the LED object like this:

LED_Ref led = LED_Init(&P2OUT,2);

This approach, however, is not as widely applicable. Unlike with the two-step solution, you cannot write LED_Init as a function if a macro-based implementation proves impossible. As mentioned in the previous section, it is far better to apply one technique consistently throughout the program; having to remember special cases greatly increases the chance of making a mistake.

Another solution is to emulate heap allocation in some simplified way. Example 3 shows one alternative.

#define MAX_LEDS 5  /* Maximum number of LED objects. */

struct _LED* LED_Init(volatile unsigned char* reg,
                      unsigned char mask) {
  static struct _LED mem[MAX_LEDS];  /* The "heap". */
  static unsigned index = 0;         /* Index of the first 
                                        free slot in the "heap". */

  if (index >= MAX_LEDS) {           /* No more room left; */
    return 0;                        /* return the null pointer. */ 
  } else {                    
    mem[index].reg = reg;            /* Initialize the newly */
    mem[index].mask = mask;          /* allocated object. */ 

    ++index;                         /* Prepare for the next allocation. */
    return &mem[index-1];            /* Return the allocated object. */
  
  }

}

Example 3. Simulating heap allocation

Changes to the static array mem persist from one call to LED_Init to the next. Note that this example does not allow you to free the allocated object.

The code in Example 3 shares some of the same dangers as heap-based allocation--namely running out of memory. You need to check the return value of LED_Init. If it is zero, then the constructor did not have enough storage to create the object.

The approach in Example 3 is also potentially wasteful, because the mem array has room for five LED objects. All of this space must still be reserved, even if the client code does not use all five objects.

Nevertheless, many embedded programs can benefit significantly from the careful and limited use of special-purpose dynamic memory allocation techniques. The next article will show such an example.

As a final note, remember that local, nonstatic variables also require a form of dynamic memory allocation! For example, if you declare a variable inside a function, the compiler will generate code to dynamically allocate the required space from the stack when the function starts. The compiler will also include code to free the space when the function exits.

The automatic way in which the system manages memory for local variables makes them far safer than heap-allocated variables. Nevertheless, it is still possible to run out of memory--with disastrous results. For this reason, you should avoid allocating large constructs as local variables.

Automatic array variables with many elements are usually what causes the system to run out of stack memory. If you are using such arrays in your functions, you can often redesign the program to declare the arrays static. This is usually the safest approach, and it also avoids time-consuming array initialization on every function call.

Pages: 1, 2, 3, 4, 5, 6

Next Pagearrow





Sponsored by: