Judgment calls - Embedded.com

Judgment calls

Writing better hardware interfaces may require writing fairly elaborate declarations. It's probably worth the effort.

About a year and a half ago, I wrote a column explaining some common techniques for representing and manipulating memory-mapped devices in C.1 I followed that with another column explaining an alternative using classes in C++.2 Those initial articles left a lot of details unresolved. Most of the columns I've written since then have been about filling in those missing details.

Nearly all of the techniques I've presented are viable in that real programmers use them in real projects and find the resulting machine code to be adequately fast and compact. Nonetheless, I've suggested that some alternatives are generally better than others, usually on the basis that the better ones yield interfaces that are easier to use correctly and harder to use incorrectly.3

Sometimes writing better interfaces requires writing fairly elaborate declarations to model the hardware. Some programmers see such declarations as not worth the bother and opt for something simpler. This month, I'll explain why I don't think that's a winning approach.

A classic approach

C programmers often define symbols for device register addresses as clusters of related macros. For example:


// timer registers
#define TMOD ((uint32_t volatile *)0xFFFF6000)
#define TDATA ((uint32_t volatile *)0xFFFF6004)
#define TCNT ((uint32_t 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, for some hypothetical programmable timer. These macros might be accompanied by additional constants for manipulating the registers, such as:


#define TE 0x01

which defines TE as a mask for setting and clearing the timer enable bit in the TMOD register. Together, all these macros represent the software interface to the timer.

Using this interface, you can disable the timer using an expression such as:


*TMOD &= ~TE;

Unfortunately, when you're putting together thousands of lines of code controlling many devices, you can easily forget to invert the mask, as in:


*TMOD &= TE;

or accidentally name the wrong register, as in:


*TDATA &= ~TE;

If you make such mistakes, the code will compile nonetheless, and you'll get to experience the joy of debugging it.

A better interface
A structure with accompanying functions provides a better interface for the timer than a collection of macros. For example, you can define the timer as:


typedef uint32_t volatile device_register;

typedef struct timer_type timer_type;
struct timer_type
{
device_register TMOD;
device_register TDATA;
device_register TCNT;
};

#define TE 0x01

along with functions that provide basic operations for programming a timer, such as:


inline void timer_disable(timer_type *t)
{
t->TMOD &= ~TE;
}

inline void timer_enable(timer_type *t);
{
t->TMOD |= TE;
}

You can map the timer_type into memory in a variety of ways.4 If you use a pointer declaration, such as:


#define the_timer ((timer_type *)0xFFFF6000)

you can disable the timer very simply, by calling:


timer_disable(the_timer);

This functional interface is better than a collection of macros representing register addresses and masks because it's easier to use correctly. Moreover, this interface works well whether there are many timers or just one.

An even better interface
A C++ class definition for the timer might look like:


class timer_type
{
public:
enum { TICKS_PER_SEC = 50000000 };
typedef uint32_t count_type;
void disable();
void enable();
void set(count_type c);
count_type get() const;
private:
enum { TE = 0x01 };
device_register TMOD;
device_register TDATA;
device_register TCNT;
};

A well-written C++ class provides an even better interface than a C structure because the class is harder to use incorrectly. (Further enhancements to this class would make it even harder to use incorrectly.) With a structure, you can easily bypass the timer functions and do something to a timer register that you'll probably regret. With a class, you have to go out of your way to access the device registers in ways other than those permitted by the public class members.
An extra line of defense
A few readers have observed–rightfully so–that compilers might pack or pad structure members so that the members don't overlay the device registers as expected. As I've noted in some previous columns, most of my examples used the simplifying assumption that the device registers are full four-byte (32-bit) words aligned to an address that's a multiple of four. Alignment issues aren't relevant to most of the issues I've discussed this past year or so, and I didn't want to get distracted.

Although member alignment is a real concern when using structures, it's hardly a show-stopper. Nearly all C and C++ compilers provide facilities, albeit non-standard ones, to control structure packing. For example, with some compilers you can use a pragma directive such as:


#pragma pack(push, 4)
struct timer_type
{
~~~
};

With other compilers, you might use something like:


struct timer_type
{
~~~
} __attribute__ ((__packed__));

Whatever notation you use, you might still be uncertain that you've aligned your registers correctly. As I explained some years ago, you can use compile-time assertions to verify that your structure or class members have the necessary offsets.5 A compile-time assertion is a statement such as:


compile_time_assert(condition);

which does nothing if condition is true and generates a compile error if condition is false. In either event, it generates no code.

The new 2011 C++ Standard introduces a built-in form of compile-time assertion, but if your compiler doesn't support it, you can use the one from the open-source Boost library (www.boost.org) or roll you own using a macro as I showed in that earlier column.

Using my compile_time_assert macro, you can verify the offset of each timer register as follows:


typedef struct timer_type timer_type;
struct timer_type
{
device_register TMOD;
device_register TDATA;
device_register TCNT;
};
compile_time_assert(offsetof(timer_type,TDATA) == 0x04);
compile_time_assert(offsetof(timer_type,TCNT) == 0x08);
compile_time_assert(sizeof(timer_type) == 0x12);

You can use the same assertions with a class as well, as long as the class is a standard-layout class , something I plan to discuss in an upcoming column. No matter how you fiddle with the structure or class definition to pack or pad the members, the assertions prevent the code from compiling unless the offsets are correct.

Writing the assertions in addition to a structure or class definition is clearly more work than writing the original bare-bones macro definitions:


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

So why bother with the structure or class? The structure yields a better interface–one that's easier to use correctly. The class is even better because it's harder to use incorrectly. For each device, you define the register layout only once. If the definitions are bit more complicated, so be it. It's a one-time cost. However, you could easily wind up accessing the device from many places in your code, offering repeated opportunities to make mistakes. Chances are the initial investment in a better interface will pay you back over time.

The call is still yours
I don't mean to suggest that my preferences are always best. Hey, we're talking about embedded systems here, where one size rarely fits all. You may have to use tools and components that don't support certain techniques very well, in which case you have to adjust what you do to the realities of what you have to work with. What works well in one environment may be too cumbersome or inefficient in another.

For each problem I tackle in this column, I try to explain a variety of techniques that will solve the problem and how you might decide which choice is best for you. I often state my preferences, but mine don't have to be yours.

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 .

Endnotes:
1. Saks, Dan. “Alternative models for memory-mapped devices,”Embedded Systems Design, May 2010, p. 9. www.eetimes.com/4027659.
2. Saks, Dan. “Memory-mapped devices as C++ classes,” Embedded.com, June 2010. www.eetimes.com/4200572.
3. Meyers, Scott. “The Most Important Design Guideline?” IEEE Software, July/August 2004, p.14. www.aristeia.com/Papers/IEEE_Software_JulAug_2004_revised.htm.
4. Saks, Dan. “Compared to what?” Embedded.com, August 2010. www.eetimes.com/4205983.
5. Saks, Dan. “Catching errors early with compile-time assertions,” Embedded Systems Programming , July 2005, p.7. www.eetimes.com/4025549.

Leave a Reply

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