In my column last May, I presented some common alternatives for representing and manipulating memory-mapped devices in C. I recommended using a structure to represent each device's collection of registers as a distinct type.1 In June, I explained why C++ classes are even better than C structures for representing memory-mapped devices.2
In Standard C and C++, you can't declare a memory-mapped object at a specified absolute address, but you can initialize a pointer with the value of that address. Then you can access that object by dereferencing the pointer. That's what I did in those articles last spring.
Several readers posted comments on Embedded.com about both my May and June columns. A few of those comments alleged that using a pointer to access a C++ class object representing a memory-mapped device incurs a performance penalty by somehow adding unnecessary indirect addressing, yet no one complained that using a pointer to access a C structure incurs a similar performance penalty. Nonetheless, I decided that it was worth investigating whether the unnecessary indirection is a problem in either language.
In my August column, I described available C and C++ features (both standard and non-standard) for placing objects into memory-mapped locations.3 In September, I presented alternative data representations for memory-mapped device registers in both C and C++ that eliminate the need to use pointers to access memory-mapped devices.4
As I explained at the end of my September column, I've run some timing tests on memory-mapped data accesses using a few different C and C++ compilers. I had expected to publish those results this month. However, as I reviewed the data, I noticed a few unexpected blips. Upon closer inspection, I discovered some inconsistencies in how I implemented a few test cases. This led me to refine some of the techniques I presented in September.
This month, I'll revisit the memory-mapped data representations that don't use pointers and explain how slightly different packaging might, on some processors, lead to better or worse performance.
Monostate vs. polystate
Back in June, I presented a C++ class called timer_registers that encapsulates the entire collection of registers for a hypothetical timer as a single abstract type. A simplified version of the class definition appears in Listing 1 .
Click on image to enlarge.
All of the member functions in the timer_registers class are “ordinary”—neither static nor virtual. Every ordinary C++ class member function is conceptually equivalent to a C (non-member) function with an additional parameter. That additional parameter is a pointer to the object (in this case, the timer) upon which the member function acts. Every ordinary member function call passes a value for the pointer. When readers expressed concern about C++ classes incurring unnecessary run-time costs, they were talking about the cost of passing and using this pointer.
In September, I showed how you can avoid passing and dereferencing those pointers by using static members in the class implementation. In particular, you can rewrite the timer_registers class so that every data member and every member function is declared static , as in Listing 2 .
Click on image to enlarge.
This implementation is appropriate only when you're using a single timer.
This implementation is actually an application of a software design pattern called the monostate pattern. This is not one of the original “Gang of Four” patterns, but it's become fairly well known.5 My monostate implementation is subtly different from traditional monostate implementations.6,7 A traditional monostate class uses ordinary (non-static) member functions, so that it behaves more like an ordinary class. Unfortunately, using ordinary member functions preserves the compiler-generated pointer parameters. My implementation uses static member functions to eliminate those compiler-generated parameters.
As far as I know, there's no specific classification for a class, such as the timer_registers class in Listing 1, that's not a monostate. When I need to identify a class as having a non-monostate implementation, I call it a polystate class . (The prefix poly- means “many”, in contrast to mono- which means “one”.)
A program can create multiple instances of a polystate class. Each such instance has its own state (its own set of data members). When your hardware has more than one of a given device, say four timers or two serial ports, you should use a polystate implementation.
A program can also “create” multiple objects of a given monostate class; however, all those monostate objects share the same statically-allocated state. You might reasonably consider attempting to create multiple instances of a monostate to be a program error. There are techniques you can use to prevent this, but they're a topic for another day.
Unbundled vs. bundled monostates in C++
The declaration for a static data member appearing inside a class is just a declaration. It doesn't allocate storage. If your program uses a static data member but doesn't provide a definition for it somewhere outside the class, the linker will flag that member as an unresolved external symbol.
The static data members in the monostate timer_registers class represent the actual memory-mapped timer registers. Those members should be bound to the appropriate memory addresses for those device registers. As I explained in my August column, there's no way to do this in Standard C++.
If your compiler supports a language extension that lets you place objects at a specific address, you should be able to apply it to static data members, as in:
static device_register TMOD @ 0xFFFF6000;
static device_register TDATA @ 0xFFFF6004;
static device_register TCNT @ 0xFFFF6008;
Otherwise, you must associate the members with their memory-mapped addresses using the linker. Some linkers provide a command-line option such as:
When the symbol is the name of a C++ static member, you can't use just the member name by itself. That is, you can't write just:
Rather you must use the member's fully-qualified name. If you're very lucky, the linker will accept the fully-qualified name exactly as it would appear in the source code, namely as: timer_registers::TMOD . More likely, the compiler will insist that you use the compiler-generated “mangled” name in the linker command option.8 The linker command option might look something like:
Specifying an address for each device register, whether by a language extension or by the linker, is tedious and error-prone.
As an alternative, you can bundle the static data members into a nested structure, as in:
static bundle b @ 0xFFFF6000;
If your compiler doesn't support the @ notation (or something like it), you can omit that part of the declaration and use the linker to specify an address for timer_registers::b .
A monostate class that wraps its register members in a nested structure is a bundled monostate . A monostate class that doesn't use a nested structure is an unbundled monostate . (I haven't seen these concepts anywhere else, so I had to invent these names as well.)
When the static data members are unbundled, the compiler can't tell that the members are actually defined nearby each other. Again, the declarations of the static members within the class are just declarations; the definitions appear elsewhere and there's nothing that says those members must be defined together. Thus, the compiler will likely assume that the members might be widely spaced and use a separate address computation to locate each unbundled member.
When the static data members are bundled, the compiler can see that each member is at a known offset from the base address of the entire collection. Hence, the compiler may be able to use a “base+offset” computation to locate each member. On some architectures, this will be more efficient than separate address calculations. I observed this in some of my tests.
Unbundled vs. bundled monostates in C
In C, you implement monostate “classes” using global variables. However, you still have a choice of unbundled or bundled implementations.
In an unbundled C implementation, you declare each device register as a separate global object, as in:
/* timer.h */
typedef uint32_t count_type;
extern device_register TMOD;
extern device_register TDATA;
extern device_register TCNT;
In a bundled C implementation, you declare the device registers within a structure, and declare a single global object for the entire collection of registers, as in:
/* timer.h */
typedef uint32_t count_type;
extern timer_bundle the_timer;
In my experiments, the performance advantage of a bundled monostate implementation compared to an unbundled implementation was the same in C as it was in C++.
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 . .
1. Saks, Dan. “Alternative models for memory-mapped devices,” Embedded Systems Design , May 2010, p. 9. www.embedded.com/columns/224700534.
2. Saks, Dan. “Memory-mapped devices as C++ classes,” Embedded.com, June 2010. www.eetimes.com/discussion/other/4200572/Memory-mapped-devices-as-C–classes.
3. Saks, Dan. “Compared to what?” Embedded.com, August 2010. www.eetimes.com/discussion/other/4205983/Compared-to-what.
4. Saks, Dan. “Accessing memory-mapped classes directly.” Embedded Systems Design , September 2010, p. 9. www.eetimes.com/discussion/programming-pointers/4208573/Accessing-memory-mapped-classes.
5. Gamma, Eric, et al. Design Patterns. Addison-Wesley, 1995.
6. Ball, Steve and John Crawford. “Monostate Classes: The Power of One.” The C++ Report, May 1997, Volume 9, Number 5, p. 30.
7. Kalev, Danny. “C++ Reference Guide: Monostate Pattern.” InformIT, October 31, 2003. www.informit.com/guides/content.aspx?g=cplusplus&seqNum=147.
8. Saks, Dan. “Function Signatures and Name Mangling.” Embedded SystemsProgramming , August 1999, p. 79.