原创 Alternative models for memory-mapped devices (Part 1)

2011-3-22 16:13 2873 12 12 分类: 消费电子

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. In my future articles, 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:

 

inline

void 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 registers

typedef uint32_t volatile timer_register;

#define TMOD ((timer_register *)0xFFFF6000)

#define TDATA ((timer_register *)0xFFFF6004)

~~~

// UART 0 registers

typedef 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.

 

 

[Continued at Alternative models for memory-mapped devices (Part 2)]

 

PARTNER CONTENT

文章评论0条评论)

登录后参与讨论
我要评论
0
12
关闭 站长推荐上一条 /3 下一条