Register Access in C++

A robust technique provides safe and efficient hardware register access.


May 01, 2005
URL:http://www.drdobbs.com/jvm/service-component-architectures/architecture-and-design/register-access-in-c/184401954

Embedded programmers traditionally use C as their language of choice. And why not? It's lean and efficient, and lets you get as close to the metal as you want. Of course C++, used properly, provides the same level of efficiency as the best C code. Moreover, you can also leverage powerful C++ features to write cleaner, safer, more elegant low-level code. In this article, I present a C++ scheme for accessing hardware registers in an optimal way.

Most embedded code needs to service hardware directly. This seemingly magical act is not that hard. Some kinds of registers need a little more fiddling to get at than others, but you certainly don't need an eye-of-newt or any voodoo dances. The exact mechanism depends on how your circuit board is wired up. The common types of register access are:

Each device has a data sheet that describes (among other things) the registers it contains, what they do, and how to use them. Registers are a fixed number of bits wide; this is usually determined by the type of device you are using. This is an important fact to know: Some devices will lock up if you write the wrong width data to them. With fixed-width registers, many devices cram several bits of functionality into one register as a "bitset." The data sheet would describe this diagrammatically.

So what does hardware access code look like? Using the example of a hypothetical UART line driver device, the traditional C-style schemes are:

It remains to be seen how to manipulate registers containing a bitset. Conventionally, you write such code by hand, something like Listing Six. This is a sure-fire way to cause yourself untold grief, tracking down odd device behavior. It's easy to manipulate the wrong bit and get very confusing results.

Listing Six

#define UART_RX_BYTES 0x0e
uint32_t uart_read()
{
    while ((*UART_RXCTL & UART_RX_BYTES) == 0) // manipulate here
    {
        ; // wait
    }
    return *UART_RXBUF;
}

Does all this sound messy and error prone? Welcome to the world of hardware devices. And this is just addressing the device: What you write into the registers is your own business, and part of what makes device control so painful. Data sheets are often ambiguous or miss essential information, and devices magically require registers to be accessed in a certain order. There will never be a silver bullet and you'll always have to wrestle these demons. All I can promise is to make the fight less biased to the hardware's side.

A Note About Using volatile

This low-level purgatory is where you use C's volatile keyword. volatile signals to the compiler that a value may change under the code's feet, that you can make no assumptions about it, and that the optimizer can't cache it for repeated use.

This is just the behavior you need for hardware register access. Every time you write code that accesses a register, you want it to result in a real register access. Don't forget the volatile qualification!

A More Modern Solution

So having seen the state of the art, at least in the C world, how can you move into the 21st century? Being a good C++ citizen, you'd ideally avoid all that nasty preprocessor use and find a way to insulate us from our own stupidity. By the end of the article, you'll have seen how to do all this and more. The real beauty of the following scheme is its simplicity. It's a solid, proven approach and has been used for the last five years in production code deployed in tens of thousands of units across three continents. Here's the recipe:

The first step is to junk the whole preprocessor macro scheme and define the device's registers in a good old-fashioned enumeration. For the moment, I'll call this enumeration Register. Although you immediately lose the ability to share definitions with assembly code, this was never a compelling benefit anyway. The enumeration values are specified as offsets from the device's base memory address. This is how they are presented in the device's data sheet, which makes it easier to check for validity. Some data sheets show byte offsets from the base address (so 32-bit register offsets increment by 4 each time), while others show "word" offsets (so 32-bit register offsets increment by 1 each time). For simplicity, I write the enumeration values however the data sheet works.

The next step is to write an inline regAddress function that converts the enumeration to a physical address. This function is a simple calculation determined by the type of offset in the enumeration. For the moment, presume that the device is memory mapped at a known fixed address. This implies the simplest MMU configuration, with no virtual memory address space in operation. This mode of operation is not at all uncommon in embedded devices. Putting all this together results in Listing Seven.

Listing Seven

static const unsigned int baseAddress = 0xfffe0000;
enum Registers
{
    STATUS = 0x00, // UART status register
    TXCTL  = 0x01, // Transmit control
    RXCTL  = 0x02, // Receive control
    ... and so on ...
};

inline volatile uint8_t *regAddress(Registers reg)
{
    return reinterpret_cast<volatile uint8_t*>(baseAddress + reg);
}

The missing part of this puzzle is the method of reading/writing registers. I do this with two simple inline functions—regRead and regWrite (Listing Eight). Being inline, all these functions can work together to make neat, readable register access code with no runtime overhead whatsoever. That's mildly impressive, but you can do so much more.

Listing Eight

inline uint8_t regRead(Registers reg)
{
    return *regAddress(reg);
}

inline void regWrite(Registers reg, uint8_t value)
{
    *regAddress(reg) = value;
}

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.

Listing Nine

// New enums for each register width
enum Registers8
{
    STATUS = 0x00, // UART status register
    ... and so on ...
};
enum Registers32
{
    TXBUF = 0x04,  // Transmit buffer
    ... and so on ...
};
// Two overloads of regAddress
inline volatile uint8_t *regAddress(Registers8 reg)
{
    return reinterpret_cast<volatile uint8_t*>(baseAddress + reg);
}
inline volatile uint32_t *regAddress(Registers32 reg)
{
    return reinterpret_cast<volatile uint32_t*>(baseAddress + reg);
}
// Two overloads of regRead
inline uint8_t regRead(Registers8 reg)
{
    return *regAddress(reg);
} 
inline uint32_t regRead(Registers32 reg)
{
    return *regAddress(reg);
}
 ... similarly for regWrite ...

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.

Listing Ten

// A macro that defines enumeration values for a bitset
// You supply the start and end bit positions
#define REG_BIT_DEFN(start, end) ((start<<16)|(end-start+1))
enum STATUS_bits
{
    TX_BUFFER_EMPTY = REG_BIT_DEFN(0, 0),
    RX_BUFFER_EMPTY = REG_BIT_DEFN(1, 1), 
    TX_UNDERRUN     = REG_BIT_DEFN(2, 2), 
    RX_OVERFLOW     = REG_BIT_DEFN(3, 3)
};
 ... similarly for other bitsets ...
#undef REG_BIT_DEFN

inline uint32_t bitRead(Registers32 reg, uint32_t bits)
{
    uint32_t       regval = *regAddress(reg);
    const uint32_t width  = bits & 0xff;
    const uint32_t bitno  = bits >> 16;
    regval >>= bitno;
    regval  &= ((1<<width)-1);
    return regval;
}
inline void bitWrite(Registers32 reg, uint32_t bits, uint32_t value)
{
    uint32_t           regval = *regAddress(reg);
    const uint32_t     width  = bits & 0xff;
    const uint32_t     bitno  = bits >> 16;
    regval &= ~(((1<<width)-1) << bitno);
    regval |=  value << bitno;
    *regAddress(reg) = regval;
}

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:

uint32_t value = regRead(DevA::MYREGISTER); 
// note: not DevA::regRead(...)

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:

Listing Eleven

// Template versions of bitRead/Write - put them at global scope
// and you don't have to copy bitRead/Write into every device namespace
template <typename RegType>
inline uint32_t bitRead(RegType reg, uint32_t bits)

    uint32_t       regval = *regAddress(reg);
    const uint32_t width  = bits & 0xff;
    const uint32_t bitno  = bits >> 16;
    regval >>= bitno;
    regval  &= ((1<<width)-1);
    return regval;
}
template <typename RegType>
inline void bitWrite(RegType reg, uint32_t bits, uint32_t value)
{
    uint32_t           regval = *regAddress(reg);
    const uint32_t     width  = bits & 0xff;
    const uint32_t     bitno  = bits >> 16;
    regval &= ~(((1<<width)-1) << bitno);
    regval |=  value << bitno;
    *regAddress(reg) = regval;
}

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.


Pete Goodliffe is a senior C++ programmer and columnist for the ACCU.

May, 2005: Register Access in C++

Listing 1

*((volatile uint32_t *)0xfffe0004) = 10;
*((volatile uint8_t  *)0xfffe0001) = 3;

May, 2005: Register Access in C++

Listing 10

// A macro that defines enumeration values for a bitset
// You supply the start and end bit positions
#define REG_BIT_DEFN(start, end) ((start<<16)|(end-start+1))
enum STATUS_bits
{
    TX_BUFFER_EMPTY = REG_BIT_DEFN(0, 0),
    RX_BUFFER_EMPTY = REG_BIT_DEFN(1, 1), 
    TX_UNDERRUN     = REG_BIT_DEFN(2, 2), 
    RX_OVERFLOW     = REG_BIT_DEFN(3, 3)
};
 ... similarly for other bitsets ...
#undef REG_BIT_DEFN

inline uint32_t bitRead(Registers32 reg, uint32_t bits)
{
    uint32_t       regval = *regAddress(reg);
    const uint32_t width  = bits & 0xff;
    const uint32_t bitno  = bits >> 16;
    regval >>= bitno;
    regval  &= ((1<<width)-1);
    return regval;
}
inline void bitWrite(Registers32 reg, uint32_t bits, uint32_t value)
{
    uint32_t           regval = *regAddress(reg);
    const uint32_t     width  = bits & 0xff;
    const uint32_t     bitno  = bits >> 16;
    regval &= ~(((1<<width)-1) << bitno);
    regval |=  value << bitno;
    *regAddress(reg) = regval;
}

May, 2005: Register Access in C++

Listing 11


// Template versions of bitRead/Write - put them at global scope
// and you don't have to copy bitRead/Write into every device namespace
template <typename RegType>
inline uint32_t bitRead(RegType reg, uint32_t bits)

    uint32_t       regval = *regAddress(reg);
    const uint32_t width  = bits & 0xff;
    const uint32_t bitno  = bits >> 16;
    regval >>= bitno;
    regval  &= ((1<<width)-1);
    return regval;
}
template <typename RegType>
inline void bitWrite(RegType reg, uint32_t bits, uint32_t value)
{
    uint32_t           regval = *regAddress(reg);
    const uint32_t     width  = bits & 0xff;
    const uint32_t     bitno  = bits >> 16;
    regval &= ~(((1<<width)-1) << bitno);
    regval |=  value << bitno;
    *regAddress(reg) = regval;
}

May, 2005: Register Access in C++

Listing 2

#define UART_TXBUF 0xfffe0004
#define UART_TXCTL 0xfffe0001
*(volatile uint32_t *)UART_TXBUF = 10;
*(volatile uint8_t  *)UART_TXCTL = 3;

May, 2005: Register Access in C++

Listing 3

#define UART_TXBUF ((volatile uint32_t*) 0xfffe0004)
#define UART_TXCTL ((volatile uint8_t*)  0xfffe0001)
*UART_TXBUF = 10;
*UART_TXCTL = 3;

May, 2005: Register Access in C++

Listing 4

extern volatile uint32_t UART_TXBUF;
extern volatile uint8_t  UART_TXCTL;
UART_TXBUF = 10;
UART_TXCTL = 3;

// compile this with:
//   gcc listing4.c
//       -gUART_UART_TXBUF=0xfffe0004 
//       -gUART_TXCTL=0xfffe0001

May, 2005: Register Access in C++

Listing 5

struct uart_device_t
{
    uint8_t STATUS;
    uint8_t TXCTL;
    ... and so on ...
};
static volatile uart_device_t * const uart_device
   = reinterpret_cast<volatile uart_device_t *>(0xfffe0000);
uart_device->TXBUF = 10;
uart_device->TXCTL = 3;

May, 2005: Register Access in C++

Listing 6

#define UART_RX_BYTES 0x0e
uint32_t uart_read()
{
    while ((*UART_RXCTL & UART_RX_BYTES) == 0) // manipulate here
    {
        ; // wait
    }
    return *UART_RXBUF;
}

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.