C/C++

Register Access in C++

Different Width Registers

Up to now, you could achieve the same effect in C with judicious use of macros. I've not yet presented anything groundbreaking. But if your device has some 8-bit registers and some 32-bit registers, you can describe each set in a different enumeration. Let's imaginatively call these Register8 and Register32. Thanks to C++'s strong typing of enums, you can now overload the register access functions, as in Listing Nine.

Now things are getting interesting: You still need only type readRead to access a register, but the compiler automatically ensures that you get the correct width register access. The only way to do this in C is manually, by defining multiple read/write macros and selecting the correct one by hand each time. This overloading shifts the onus of knowing which registers require 8- or 32-bit writes from programmers using the device to the compiler. A whole class of error silently disappears.

Accessing Bitsets

You can make it easier and safer to write values into register bitsets. Usually you only ever need to read or set a few bits at a time. Manually crafting the bit-twiddling logic leads to hard-to-follow code and is also very error prone. Ideally, you'd provide a set of simple functions for this operation, akin to regRead and regWrite, that can manipulate the individual register bits. The only question is how to specify which bits to read/write.

You construct each bitset definition in an enumeration by encoding the starting bit position and the number of relevant bits in the enumeration values. Unfortunately, you have to use a macro to do this (there's no reasonable alternative; enumeration values can only be integer constants so you can't use a helper function here).

The inline functions bitRead and bitWrite are defined to decode the enumeration values and act on them accordingly; see Listing Ten. The resulting code is as good as any hand-written alternative, and again you have improved code readability and safety.

Extending to Multiple Devices

An embedded system is composed of many separate devices, each performing their allotted task. Perhaps you have a UART for control, a network chip for communication, a sound device for audible warnings, and more. You need to define multiple register sets with different base addresses and associated bitset definitions. Some large devices (like super I/O chips) consist of several subsystems that work independently of one another; you'd also like to keep the register definitions for these parts distinct.

The classic C technique is to augment each block of register definition names with a logical prefix. For example, you'd define the UART transmit buffer like this:

#define MYDEVICE_UART_TXBUF ((volatile
uint32_t *)0xffe0004)

C++ provides an ideal replacement mechanism that solves more than just this aesthetic blight. You can group register definitions within namespaces. The nest of underscored names is replaced by "::" qualificationsa better, syntactic indication of relationship. Because the overload rules honor namespaces, you can never write a register value to the wrong device block: It's a syntactic error. This is a simple trick, but it makes the scheme incredibly usable and powerful.

Namespacing also lets you write more readable code with a judicious sprinkling of using declarations inside device setup functions. Koenig lookup combats excess verbiage in our code. If you have register sets in two namespaces DevA and DevB, you needn't qualify a regRead call, just the register name. The compiler can infer the correct regRead overload in the correct namespace from its parameter type. You only have to write:

Variable Base Addresses

Not every operating environment is as simplistic as discussed so far. If a virtual memory system is in use, then you can't directly access the physical memory-mapped locationsthey are hidden behind the virtual address space. Fortunately, every OS provides a mechanism to map known physical memory locations into the current process's virtual address space.

A simple modification lets you accommodate this memory indirection. You must change the baseAddress variable from a simple static const pointer to a real variable. The header file defines it as extern, and before any register accesses, you must arrange to define and assign it in your code. The definition of baseAddress will be necessarily system specific.

Other Uses

Here are a few extra considerations for the use of this register access scheme:

Just as you use namespaces to separate device definitions, it's a good idea to choose header filenames that reflect the logical device relationships. It's best to nest the headers in directories corresponding to the namespace names.

A real bonus of this register access scheme is that you can easily substitute alternative regRead/regWrite implementations. It's easy to extend your code to add register access logging, for example. I have used this technique to successfully debug hardware problems. Alternatively, you can set a breakpoint on register access, or introduce a brief delay after each write (this quick change shows whether a device needs a pause to action each register assignment).

It's important to understand that this scheme leads to larger unoptimized builds. Although it's remarkably rare to not optimize your code, without optimization inline functions are not reduced and your code will grow.

There are still ways to abuse this scheme. You can pass the wrong bitset to the wrong register, for example. But it's an order of magnitude harder to get anything wrong.

A small sprinkling of template code lets you avoid repeated definition of bitRead/bitWrite; see Listing Eleven.

Proof of Efficiency

Perhaps you think that this is an obviously good solution, or you're just presuming that I'm right. However, a lot of old-school embedded programmers are not so easily persuaded. When I introduced this scheme in one company, I met a lot of resistance from C programmers who just could not believe that the inline functions resulted in code as efficient as the proven macro technique.

The only way to persuade them was with hard dataI compiled equivalent code using both techniques for the target platform (GCC targeting a MIPS device). Table 1 lists the results. An inspection of the machine code generated for each kind of register access showed that the code was identical. You can't argue with that!

Table 1: Results of compiling equivalent code using both techniques for the target platform.

Register Access Method

Results (object file size in bytes)

Unoptimized

Optimized

C++ inline function scheme

1087

551

C++ using #defines

604

551

C using #defines

612

588

It's particularly interesting to note that the #define method in C is slightly larger than the C++ equivalent. This is a peculiarity of the GCC toolchainthe assembly listing for the two main functions is identical: The difference in file size is down to the glue around the function code.

Conclusion

Okay, this isn't rocket science, and there's no scary template metaprogramming in sight (which, if you've seen the average embedded programmer, is no bad thing!). But this is a robust technique that exploits a number of C++ features to provide safe and efficient hardware register access. Not only is it supremely readable and natural in the C++ idiom, it prevents many common register access bugs and provides extreme flexibility for hardware access tracing and debugging.

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task.
However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

Video

This month's Dr. Dobb's Journal

This month,
Dr. Dobb's Journal is devoted to mobile programming. We introduce you to Apple's new Swift programming language, discuss the perils of being the third-most-popular mobile platform, revisit SQLite on Android
, and much more!