Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C/C++

Register Access in C++


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:

  • Memory-mapped I/O. The hardware lets you communicate with a device using the same instructions as memory access. The device is wired up to live at memory address n; register 1 is mapped at address n, register 2 is at n+1, register 3 at n+2, and so on.
  • Port-mapped I/O. Certain devices present pages of registers that you have to map into memory by selecting the correct device "port." You might use specific input/output CPU instructions to talk to these devices, although more often the port and its selector are mapped directly into the memory address space.
  • Bus separated. It's harder to control devices connected over a nonmemory-mapped bus. I2C and I2S are common peripheral connection buses. In this scenario, you must either talk to a dedicated I2C control chip (whose registers are memory mapped), telling it what to send to the device, or you manipulate I2C control lines yourself using GPIO ports (General Purpose Input/Output, assignable control lines not specifically designed for a particular data bus) on some other memory-mapped device.

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:

  • Direct memory pointer access. It's not unheard of to see register access code similar to Listing One, but we all know that the perpetrators of this kind of monstrosity should be punished. It's neither readable nor maintainable.

    Listing One
    *((volatile uint32_t *)0xfffe0004) = 10;
    *((volatile uint8_t  *)0xfffe0001) = 3;
    
  • Pointer usage is usually made bearable by defining a macro name for each register location. There are two distinct macro flavors. The first macro style defines bare memory addresses (as in Listing Two). The only real advantage of this is that you can share the definition with assembly code parsed using the C preprocessor. As you can see, its use is long winded in normal C code, and prone to error; you have to get the cast right each time. The alternative (see Listing Three) is to include the cast in the macro itself; far nicer in C. Unless there's a lot of assembly code, this latter approach is preferable.

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


    Listing Three
    #define UART_TXBUF ((volatile uint32_t*) 0xfffe0004)
    #define UART_TXCTL ((volatile uint8_t*)  0xfffe0001)
    *UART_TXBUF = 10;
    *UART_TXCTL = 3;
    
  • Macros have no overhead in terms of code speed or size. The alternative, creating a physical pointer variable to describe each register location, would have a negative impact on both code performance and executable size. However, macros are gross and C++ programmers already smell a rat here. There are plenty of problems with this fragile scheme. It's programming at a very low level, and the code's real intent is not clear—it's hard to spot all register accesses as you browse a function.
  • Deferred assignment is a technique that lets you write code like Listing Four, defining the register location values at link time. This is not commonly used; it's cumbersome when you have a number of large devices, and not all compilers provide this functionality. It requires you to run a flat (nonvirtual) memory model.

    Listing Four
    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
    
  • Use a struct to describe the register layout in memory, as in Listing Five. There's a lot to be said for this approach—it's logical and reasonably readable. However, it has one big drawback—it is not Standards-compliant. Neither the C nor C++ Standards specify how the contents of a struct are laid out in memory. You are guaranteed an exact ordering, but you don't know how the compiler pads out nonaligned items. Indeed, some compilers have proprietary extensions or switches to determine this behavior. Your code might work fine with one compiler and produce startling results on another.

    Listing Five
    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;
    
  • Create a function to access the registers and hide all the gross stuff in there. On less speedy devices, this might be prohibitively slow, but for most applications it is perfectly adequate, especially for registers that are accessed infrequently. For port-mapped registers, this makes a lot of sense; their access requires complex logic, and writing all this out longhand is tortuous and easy to get wrong.

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;
}


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

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.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.