Alternative models for memory-mapped devices - Embedded.com

Alternative models for memory-mapped devices

Device drivers typically communicate with hardware devices through device registers. A driver sends commands or data to a device by storing into device registers, or retrieves status information or data from a device by reading from device registers.

Many processors use memory-mapped I/O , which maps device registers to fixed addresses in the conventional memory space. To a C or C++ programmer, a memory-mapped device register looks very much like an ordinary data object. Programs can use built-in operators such as assignment to move values to or from memory-mapped device registers.

Some processors use port-mapped I/O , which maps device registers to addresses in a separate address space, apart from the conventional memory space. Port-mapped I/O usually requires special machine instructions, such as the in and out instructions of the Intel x86 processors, to move data to or from device registers. To a C or C++ programmer, port-mapped device registers don't look like ordinary memory.

The C and C++ standards say nothing about port-mapped I/O. Programs that perform port-mapped I/O must use nonstandard, platform-specific language or library extensions, or worse, assembly code. On the other hand, programs can perform memory-mapped I/O using only standard language features. Fortunately, port-mapped I/O appears to be gradually fading away, and fewer programmers need to fuss with it.

Several years ago, I wrote a series of articles on accessing memory-mapped device registers using C and C++.1, 2, 3 At the time, I focused on how to set up pointers (in C or C++) or references (in C++) to enable access to memory-mapped registers. I followed those articles with another discussing some alternatives for choosing the types that represent the registers themselves.4

In the years since then, I've had many discussions with readers and conference attendees who use other techniques for representing device registers. I find that many programmers are still using approaches that are inconvenient and error-prone. This month, I'll compare some popular alternatives, focusing more on the interface design issues than on the implementation details.

Mapping memory the old-fashioned way
Let's consider a machine with a variety of devices, including a programmable timer and a couple of UARTs (serial ports), each of which employs a small collection of device registers. The timer registers start at location 0xFFFF6000. The registers for UART0 and UART1 start at 0xFFFFD000 and 0xFFFFE000, respectively.

For simplicity, let's assume that every device register is a four-byte word aligned to an address that's a multiple of four, so that you can manipulate each device register as an unsigned int . Many programmers prefer to use an exact-width type such as uint32_t . (Types such as uint32_t are defined in the C99 header .)5

I prefer to use a symbolic type whose name conveys the meaning of the type rather than its physical extent, such as:

typedef uint32_t device_register;   

Device registers are actually volatile entities-they may change state in ways that compilers can't detect.6, 7 I often include the volatile qualifier in the typedef definition, as in:

typedef uint32_t volatile device_register;   

I've often seen C headers that define symbols for device register addresses as clusters of related macros. For example:

// timer registers#define TMOD  ((unsigned volatile *)0xFFFF6000)#define TDATA ((unsigned volatile *)0xFFFF6004)#define TCNT  ((unsigned volatile *)0xFFFF6008)   

defines TMOD , TDATA , and TCNT as the addresses of the timer mode register, the timer data register, and the timer count register, respectively. The header might also include useful constants for manipulating the registers, such as:

#define TE 0x01#define TICKS_PER_SEC 50000000   

which defines TE as a mask for setting and clearing the timer enable bit in the TMOD register , and TICKS_PER_SEC as the number of times the TCNT register decrements in one second.

Using these definitions, you can disable the timer using an expression such as:

*TMOD &= ~TE;   

or prepare the timer to count for two seconds using:

*TDATA = 2 * TICKS_PER_SEC;*TCNT = 0;   

Other devices require similar clusters of macros, such as:

// UART 0 registers#define ULCON0  ((unsigned volatile *)0xFFFFD000)~~~#define USTAT0  ((unsigned volatile *)0xFFFFD008)#define UTXBUF0 ((unsigned volatile *)0xFFFFD00C)~~~// UART 1 registers#define ULCON1  ((unsigned volatile *)0xFFFFE000)~~~#define USTAT1  ((unsigned volatile *)0xFFFFE008)#define UTXBUF1 ((unsigned volatile *)0xFFFFE00C)~~~   

In this case, UART0 and UART1 have identical sets of registers, but at different memory-mapped addresses. They can share a common set of bit masks:

#define RDR 0x20#define TBE 0x40   

So what's not to like?
This approach leaves a lot to be desired. As Scott Meyers likes to say, interfaces should be “easy to use correctly and hard to use incorrectly.”8 Well, this scheme leads to just the opposite.

By itself, disabling a timer isn't a hard thing to do. When you're putting together a system with thousands of lines of code controlling dozens of devices, writing:

*TMOD &= ~TE;   

is asking for trouble. You could easily write |= instead of &= , leave off the ~, or select the wrong mask. Even after you get everything right, the code is far from self-explanatory.

Rather, you should package this operation as a function named timer_disable , or something like that, so that you and your cohorts can disable the timer with a simple function call. The function body is very short–it's only a single assignment–so you should probably declare it as an inline function. If you're stuck using an older C dialect, you can make it a function-like macro.

But what should you pass to that call? Most devices have several registers. Is it really a good idea to make the caller responsible for selecting which of the timer registers to pass to the function, as in:

timer_disable(TMOD);   

when only one register will do? Maybe it's better to just build all the knowledge into the function, as in:

inlinevoid timer_disable(void)    {    *TMOD &= ~TE;    }   

so that all you have to do is call:

timer_disable();   

This works for the timer because there's only one timer. However, my sample machine has two UARTs, each with six registers, and many UART operations employ more than one register.

For example, to send data out a port, you must use both the UART status register and the transmit buffer register. The function call might look like:

UART_put(USTAT0, UTXBUF0, c);   

Maybe passing two registers isn't that bad, but what about operations that require three or even four registers?

Beyond the inconvenience, calling functions that require multiple registers invites you to make mistakes such as:

UART_put(USTAT0, UTXBUF1, c);   

which passes registers from two different UARTs. In fact, the function even lets you accidentally pass a timer register to a UART operation, as in:

UART_put(USTAT, TDATA, c);   

Ideally, compilers should catch these errors, but they can't. The problem is that, although each macro has a different value, they all yield expressions of the same type, namely, “pointer to volatile unsigned.” Consequently, compilers can't use type checking to tell them apart.

Using different typedefs doesn't help. A typedef doesn't define a new type; it's just an alias for some other type. Thus, even if you define the registers as:

// timer registerstypedef uint32_t volatile timer_register;#define TMOD    ((timer_register *)0xFFFF6000)#define TDATA   ((timer_register *)0xFFFF6004)~~~// UART 0 registerstypedef uint32_t volatile UART_register;#define ULCON0  ((UART_register *)0xFFFFD000)#define UCON0   ((UART_register *)0xFFFFD004)~~~   

then timer_register and UART_register are just two different names for the same type, and you can use them interchangeably throughout your code. Even if you take pains to declare the UART_put function as:

void UART_put(UART_register *s,      UART_register *b, int c);   

you can still pass it a timer register as a UART register. By any name, an volatile unsigned is still an volatile unsigned.

Again, you could write the UART functions so that they know which registers to use. But there are two UARTs, so you'd have to write pairs of nearly identical functions, such as:

void UART0_put(int c);void UART1_put(int c);   

This gets tedious quickly, and becomes prohibitive when the number of UARTs is much bigger than two.

As an alternative, you could pass an integer designating a UART, as in:

typedef unsigned UART_number;void UART_put(UART_number n, int c);   

To make this work, you need a scheme that converts integers into register addresses at run time. Plus, you have to worry about what happens when you pass an integer that's out of range.

Using structures
Structures provide a better way to model memory-mapped devices. You can use a structure to represent each collection of device registers as a distinct type. For example:

typedef uint32_t volatile device_register;typedef struct timer_registers timer_registers;struct timer_registers    {    device_register TMOD;    device_register TDATA;    device_register TCNT;    };   

The typedef before the struct definition elevates the tag name timer_registers from a mere tag to a full-fledged type name.9 It lets you refer to the struct type as just timer_registers rather than as struct timer_ registers .

You can provide corresponding structures for each device type:

typedef struct UART_registers UART_registers;struct UART_registers    {    device_register ULCON;    device_register UCON;    device_register USTAT;    device_register UTXBUF;    ~~~    };   

Using these structures, you can define pointers that let you access device registers. You can define the pointers as macros:

#define the_timer      ((timer_registers *)0xFFFF6000)#define UART0 ((UART_registers *)0xFFFFD000)   

or as constant pointers:

timer_registers *const the_timer    = (timer_registers *)0xFFFF6000;UART_registers *const UART0     = (UART_registers *)0xFFFFD000;   

In C++, using a reinterpret_cast is even better:

timer_registers *const the_timer    = reinterpret_cast         (0xFFFF6000);UART_registers *const UART0    = reinterpret_cast         (0xFFFFD000);   

Whichever way you define the pointers, you can use them to access the actual device registers.

For example, you can disable the timer using the expression:

the_timer->TMOD &= ~TE;   

Even better, you can wrap it in an inline function:

inlinevoid timer_disable(timer_registers *t)    {    t->TMOD &= ~TE;    }   

or a function-like macro:

#define timer_disable(t) ((t)->TMOD &= ~TE)   

Whether you use an inline function or a macro, you can simply call:

timer_disable(the_timer);   

For device operations that use more than one register, you can pass just the address of the entire register collection rather than individual registers. Again, sending data to a UART uses both the UART status register and the transmit buffer register. You can declare the UART_put function as:

void UART_put(UART_registers *u, int c);   

and write it so that it picks out the specific registers that it needs. A call to the function looks like:

UART_put(UART0, c);   

which is just a tad simpler than it was before. Plus, you can't accidentally mix registers from two UARTs at once.

Using structures avoid other mistakes as well. Each struct is a truly distinct type. You can't accidentally convert a “pointer to timer_registers ” into a “pointer to UART_registers .” You can only do it intentionally using a cast. Thus, compilers can easily catch accidents such as:

timer_disable(UART0);       // compile errorUART_put(the_timer, c);     // compile error   

One of the problems with using structures to model collections of memory-mapped registers is that compilers have some freedom to insert unused bytes, called padding, after structure members.10 You may have to use compile switches or pragma directives to get your structures just so.4 You can also use compile-time assertions to verify that the structure members are laid as they should be.11

Classes are even better
In C++, using a class to model hardware registers is even better than using a struct. I'll show you why over the coming months.

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 . For more information about Dan .

Endnotes:
1. Saks, Dan. “Mapping Memory,” Embedded Systems Programming , September 2004, p. 49. www.embedded.com/26807176

2. Saks, Dan. “Mapping Memory Efficiently,” Embedded Systems Programming , November 2004, p. 47. www.embedded.com/50900224

3. Saks, Dan. “More ways to map memory,” Embedded Systems Programming , January 2005, p. 7. www.embedded.com/55301821

4. Saks, Dan. “Sizing and Aligning Device Registers,” Embedded Systems Programming , May 2005, p. 9. www.embedded.com/55301821

5. Barr, Michael. “Introduction to fixed-width integers,” Embedded.com , January 2004. www.embedded.com/17300092

6. Saks, Dan.”Use Volatile Judiciously,” Embedded Systems Programming , September 2005, p. 8. www.embedded.com/170701302

7. Saks, Dan.”Place Volatile Accurately,” Embedded Systems Programming , November 2005, p. 11. www.embedded.com/174300478

8. Meyers, Scott. “The Most Important Design Guideline?” IEEE Software , July/August 2004, p.14. www.aristeia.com/Papers/IEEE_Software_JulAug_2004_revised.htm

9. Saks, Dan. “Tag Names vs. Type Names,” Embedded Systems Programming , September 2002, p. 7. www.embedded.com/9900748

10. Saks, Dan. “Padding and rearranging structure members,” Embedded Systems Design , May 2009, p. 11. www.embedded.com/217200828

11. Saks, Dan, “Catching Errors Early with Compile-Time Assertions,” Embedded Systems Programming , July 2005, p. 7. www.embedded.com/columns/164900888

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.