Memory-mapped devices as C++ classes - Embedded.com

Memory-mapped devices as C++ classes

Last month, I examined some common alternatives for representing and manipulating memory-mapped devices in C.1 The techniques I presented work as well (or as badly) in C++. However, classes in C++ offer a better alternative than anything you can do in C. This month, I'll explain why.

Picking up where I left off
As I did last month, I'll use for my examples a machine with a variety of devices, including a programmable timer, which employs a small collection of device registers. The timer registers start at location 0xFFFF6000.

For simplicity, I assume that every device register is a four-byte word aligned to an address that's a multiple of four, so that you can manipulate each device register as an unsigned int or, equivalently, as a uint32_t . Since device registers are volatile entities, I typically bundle the volatile qualifier with the appropriate integer type within a typedef definition, as in:

typedef uint32_t volatile device_register;   

In C, structures often provide the best way to model the device registers of memory-mapped devices. For example:

typedef uint32_t volatile device_register;typedef struct timer_registers timer_registers;struct timer_registers    {    device_register TMOD;    device_register TDATA;    device_register TCNT;    };   

defines the layout for a timer with three device registers.

The header (.h) file that defines this structure might also define useful constants and types for manipulating the registers, such as:

#define TE 0x01#define TICKS_PER_SEC 50000000typedef uint32_t timer_count_type;   

which defines TE as a mask for setting and clearing the timer enable bit in the TMOD register, TICKS_PER_SEC as the number of times the timer's TCNT register can decrement in one second, and timer_count_type as the type of an object that can hold the TCNT register's value. That header should also declare functions that provide basic operations for programming a timer, such as:

void timer_disable(timer_registers *t);void timer_enable(timer_registers *t);void timer_set(timer_registers *t, timer_count_type c);timer_count_type timer_get(timer_registers const *t);   

Some of the operations probably will be short and fast, so you might prefer to define them, not just declare them, in the header as either inline functions, such as:

inlinevoid timer_disable(timer_registers *t)    {    t->TMOD &= ~TE;    }   

or (with older C dialects) as function-like macros, such as:

#define timer_disable(t) ((t)->TMOD &= ~TE)   

The definitions for the non-inline functions should appear in a source (.c) file that accompanies the header.

Finally, somewhere in your program you should define a pointer to the actual device registers, either as a macro:

#define the_timer ((timer_registers *)0xFFFF6000)   

or as a constant pointer:

timer_registers *const the_timer    = (timer_registers *)0xFFFF6000;   

Then you can control the timer via function (or function-like macro) calls such as:

timer_disable(the_timer);timer_set(the_timer, TICKS_PER_SEC);timer_enable(the_timer);   

This approach–using a structure to represent the device registers and associated functions to implement device operations–does a pretty fair job of packaging the timer as an abstract type in C. This packaging technique is essentially the same as the one I described several years ago as part of a more general discussion of abstract types in C.2

Unfortunately, this implementation doesn't enforce the abstraction very well because C compilers can't prevent code from bypassing the functions and accessing the device registers directly. Using incomplete types to implement abstract types in C improves compile-time enforcement of the abstraction, but often incurs a performance penalty. This is where C++ classes have an advantage.

Hardware devices as class objects
Rewriting the timer_registers structure and functions as a C++ class is a fairly straightforward task. The mechanics of the rewrite are much as I described in an earlier column.3 The definition for a timer_registers class appears in Listing 1 .

Listing 1: A timer_registers class definition in C++.

// timer.h#ifndef TIMER_H_INCLUDED#define TIMER_H_INCLUDEDclass timer_registers    {public:    enum { TICKS_PER_SEC = 50000000 };    typedef uint32_t count_type;    void disable();    void enable();    void set(count_type c);    count_type get() const;private:    enum { TE = 0x01 };    device_register TMOD;    device_register TDATA;    device_register TCNT;    };inlinevoid timer_registers::disable()    {    TMOD &= ~TE;    }inlinevoid timer_registers::enable()    {    TMOD |= TE;    }inlinevoid timer_registers::set(count_type c)    {    TDATA = c;    TCNT = 0;    }inlinetimer_registers::count_type timer_registers::get() const    {    return TCNT;    }#endif   

All the member functions in Listing 1 , are short and simple enough to justify defining them as inline functions. If there were any non-inline functions, they would be defined in a separate source (.cpp) file.

As in C, somewhere in your C++ program you should define a pointer to the address of the actual device registers. The prevailing wisdom, with which I agree, is that you should shy away from macros and use a constant pointer, initialized using a reinterpret_cast , as in:

timer_registers *const the_timer    = reinterpret_cast(0xFFFF6000);   

Then you can control the timer via member function calls such as:

the_timer->disable();the_timer->set(timer_registers::TICKS_PER_SEC);the_timer->enable();   

Alternatively, you can use a reference instead of a pointer, defined as:

timer_registers &the_timer    = *reinterpret_cast(0xFFFF6000);   

This lets you refer to the_timer as if it were the actual device, rather than a pointer to the device, as in:

the_timer.disable();the_timer.set(timer_registers::TICKS_PER_SEC);the_timer.enable();   

I prefer the reference notation. I'll assume for the remainder of this article that the_timer is a reference, not a pointer.

The individual timer registers are private members of the timer_registers class so that any attempt to access a device register directly, as in:

the_timer.TMOD = 0;   

will provoke a compile error when it appears outside the body of a member function. Enforcing abstraction is a Good Thing, and it's something that C often can't do without sacrificing performance.4

As I mentioned earlier, code that manipulates device registers often uses integer-valued symbolic constants. C programs typically define these constants as macros, such as:

#define TE 0x01#define TICKS_PER_SEC 50000000   

Here, TE is a bit mask corresponding to the enable bit in the TMOD register. The mask is an implementation detail useful only to code that can access TMOD , but TMOD is a private member of the timer_registers class. Therefore, TE should also be a private member of timer_registers .

Unfortunately, macro names don't observe the usual scope rules of C++ (or C). Even if you place the macro definition inside the curly braces of a function or class definition, as in:

class timer_registers    {public:    ~~~private:    #define TE 0x01    device_register TMOD;    device_register TDATA;    device_register TCNT;    };   

the name TE will still be effectively global.

Fortunately, enumeration definitions do obey the scope and access rules of C++. If you define TE as an enumeration constant inside the class definition, as shown in Listing 1, then it will observe the same scope and access rules as other class members. Alternatively, you can declare TE as a static const data member, a detail I'll defer to a future column.

In contrast to TE , which is a private const, TICKS_PER_SEC is a public constant. The value of TICKS_PER_SEC isn't merely an implementation detail; it's part of the interface to the class. For example, it's useful as an argument to member function calls such as:

the_timer.set(timer_registers::TICKS_PER_SEC);   

which prepares the timer to count for one second.

Just a start
Over the last decade or so, I've found that representing hardware registers as C++ classes is a significant improvement over using C structures. However, using classes raises other issues–such as whether to use constructors–that just don't arise when using C structures. I'll have more to say about these issues in the future.

Dan Saks is president of Saks & Associates, a C/C++ training and consulting company. For more information about Dan Saks, visit his website at www.dansaks.com. Dan also welcomes your feedback: e-mail him at . For more information about Dan .

Endnotes:
1. Saks, Dan, “Alternative models for memory-mapped devices,” Embedded Systems Design , May 2010, p. 9. www.embedded.com/columns/224700534
2. Saks, Dan “Abstract types in C,” Embedded Systems Programming , November 2003, p. 39. www.embedded.com/columns/15300198
3. Saks, Dan “C++ classes as abstractions,” Embedded Systems Programming , January 2004, p. 29. www.embedded.com/columns/16700209
4. Saks, Dan “Incomplete types as abstractions,” Embedded Systems Programming , December 2003, p. 43. www.embedded.com/columns/16100434

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.