In my past articles, I examined some common alternatives for representing and manipulating memory-mapped devices in C.1 I followed that with a column on why classes in C++ offer a better alternative than anything you can do in C.2 By "better," I mean that well-written classes yield interfaces that are typically easier to use correctly and harder to use incorrectly.3
On the other hand, designing and implementing a well-written C++ class often requires attention to details that don't apply when you program in C. For example, one of the easiest ways to misuse a structure object in C is to fail to initialize it properly. In C++, a class can have special member functions, called constructors, that provide guaranteed initialization for objects of that class type. The guarantee isn't absolute—you can subvert it using a cast—but it's nonetheless effective at reducing the incidence of uninitialized objects.
At the end of my last column, I note that "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." Despite this cautionary note, some readers posted comments criticizing my coding example for not addressing initialization. They objected not only to the absence of constructors, but also to my use of casting to initialize pointers and references referring to memory-mapped class objects.
Other readers defended the code, arguing that construction and destruction are inappropriate operations for memory-mapped devices. I appreciate the backup, but I actually agree that omitting constructors and using casts can be problematic. That's why I intend to say something about them, but not just yet. Rather, I'm going to respond to a different concern first, not because it's any more important, but because the discussion provides background that will be helpful when discussing initialization issues.
Concerns about performance
Beyond their concerns about initialization and type safety, a few readers expressed concern that using a pointer to access a class object representing a memory-mapped device incurs a performance penalty by somehow adding unnecessary pointer indirection. Some considered the performance hit to be a serious problem, while others suggested it wasn't nearly so bad in practice.
Interestingly, no one complained that using C structures to represent memory-mapped devices incurs a similar performance penalty. So I wonder: Is the allegation that using a C++ class is more expensive than using a C structure? Or is it that using pointers to access memory-mapped class objects is more costly than using some other means?
If accessing memory-mapped class objects through pointers incurs a performance penalty, we should first ask "Compared to what?" In other words, if using pointers is slow, then what else is there that might be faster?
In my future articles, I'll enumerate the available alternatives for placing objects into memory-mapped locations. I'll consider alternative implementations that eliminate the need to use pointers to access memory-mapped devices in my future posts.
Placing objects into memory-mapped locations
Normally, you don't choose the memory locations where program objects reside—the compiler does, often with substantial help from the linker. For example, when the compiler encounters an object declaration at global scope such as:
int n;
the compiler sets aside so many bytes at some offset within a particular code segment. For an object declaration at local scope, the compiler sets aside so many bytes at some offset within the stack frame of the function containing the declaration.
For an object representing memory-mapped device registers, the compiler doesn't get to choose where object resides—the hardware has already chosen. Thus, to access the object, the code needs a declaration for a name it can use in a simple expression to reference the memory-mapped location as if that location were an object of the appropriate type.
Many C and C++ compilers provide language extensions that let you position an object at a specified memory address. Unfortunately, such extensions are non-standard, and nearly every compiler provides something different. For example, to declare a timer_registers object residing at location 0xFFFF6000, you might write:
timer_registers the_timer @ 0xFFFF6000;
with one compiler, or:
timer_registers the_timer _at(0xFFFF6000);
with another, or:
timer_registers the_timer
__attribute__((at(0xFFFF6000)));
with yet another. As far as I know, these kinds of declarations don't have a convenient name, so I'll make one up. Let's call them "memory-mapped object declarations."
With memory-mapped object declarations, the_timer acts like the memory-mapped object itself rather than as a pointer to that object. That is, if timer_registers is a structure in C, then you can control the timer by passing its address to a function (or function-like macro) in a call such as:
timer_enable(&the_timer);
If timer_registers is a class in C++, then you can control the timer by applying a member function to it, as in:
the_timer.enable();
If you're wedded to a particular compiler that supports some form of memory-mapped object declaration, you may find them convenient to use. If you aspire to write in a more portable style, you'll probably want to avoid writing such declarations.
As some readers suggested, you can declare a memory-mapped object using a standard extern declaration such as:
extern timer_registers the_timer;
and then use linker command options or linker scripts to force the_timer into the desired address. Although the syntax of the declaration is valid Standard C, the resulting program is not. This technique just pawns off the non-standard stuff to another tool, the linker. I agree with my colleague Bill Gatliff that it's better to keep all the ugly bits in one language.
In Standard C and C++, you can't declare an object at a specified absolute address, but you can cast an integer into a pointer. Strictly speaking, the C++ Standard says the cast has implementation-defined behavior and therefore may yield a non-portable result. In practice, the cast does what most of us expect it to do, namely, it yields a pointer to an absolute address. If that address is the address of a device register, you can dereference that pointer to access that register.
In C or C++, you can define a pointer to a device register as a macro such as:
#define the_timer ((timer_registers *)0xFFFF6000)
or as a constant pointer:
timer_registers *const the_timer
= (timer_registers *)0xFFFF6000;
C++ provides a small set of "new style" casts which offer the same functionality as the "old style" C casts, but with better type checking. In C++, the new-style cast for converting an integer into a pointer is a reinterpret_cast, as in:
timer_registers *const the_timer
= reinterpret_cast(0xFFFF6000);
On any given platform, this cast has the same implementation-defined behavior as the corresponding old-style cast.
In contrast to the earlier examples, this approach declares the_timer as a pointer to the memory-mapped object rather than as the object itself. Thus, if timer_registers is a structure in C, then you can control the timer by passing the pointer to calls such as:
timer_enable(the_timer);
If timer_registers is a class in C++, then you can control the timer by applying a member function to the object to which the_timer points, as in:
the_timer->enable();
In C, you can get the macro to behave like an object rather than like a pointer by defining it as:
#define the_timer (*(timer_registers *)0xFFFF6000)
In C++, you can use a reference to get object-like behavior:
timer_registers &the_timer
= *reinterpret_cast(0xFFFF6000);
I used the reference notation in my examples in my previous articles.
A few years ago, I explained the subtle behavioral differences between macros and constant pointers.4, 5, 6 From a design and maintenance standpoint, using constant pointers is generally better. Macros are inferior largely because they don't obey the usual scope rules. However, I found that some C compilers generated slightly smaller and faster code using macros. I found no C compilers that generated better code using constant pointers. C++ compilers appear to generate equally good code from both macros and constant pointers. Of course, your experience may be different, in which case I'd appreciate hearing from you.
Stay tuned
In my upcoming posts, I'll continue this discussion by looking at ways to implement C function libraries and C++ classes that access memory-mapped devices without using pointers.
Endnotes:
1. Saks, Dan, "Alternative models for memory-mapped devices", Eetasia.com, March 2011. http://forum.eetasia.com/BLOG_ARTICLE_6978.HTM.
2. Saks, Dan, "Memory-mapped devices as C++ classes", Eetasia.com, March 2011. http://forum.eetasia.com/BLOG_ARTICLE_7018.HTM.
3. Meyers, Scott, "The Most Important Design Guideline?" IEEE Software, July/August 2004, p.14.
4. Saks, Dan, "Mapping Memory", Embedded Systems Programming, September 2004, p. 49.
5. Saks, Dan, "Mapping Memory Efficiently", Embedded Systems Programming, November 2004, p. 47.
6. Saks, Dan, "More ways to map memory", Embedded Systems Programming, January 2005, p. 7.
文章评论(0条评论)
登录后参与讨论