Programming Pointers

Judgment calls

Dan Saks

12/6/2011 4:02 PM EST

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.

Next: Page 2




willc2010

12/8/2011 2:17 AM EST

I agree. It is usually worth putting in the effort. Many of the suggested techniques are worth considering. But step back for a moment and consider how odd it is to have to craft a register layout by a kind of approximation and then try to confirm its correctness using a macro that can expand to either a legal (but otherwise irrelevant) or an illegal array definition (or one of the other usual variants). Surely a language for embedded programming should allow the programmer to state what they want directly. Compare this with Ada, for example. A big part of writing Ada is putting in the effort up front. The difference is that Ada is designed for that style of programming, whereas C[++] isn't, so what in Ada is a direct statement often requires in C[++] a kind of hack (and an article to explain it).

Encapsulation too is generally a good idea, but the problem with classes as the mechanism is the rigid connection between the concepts of encapsulation unit and type, which is often unnecessary and forced. So, while I agree with encapsulation, the whole business of needing to overload 'new' to do it is a complication. And, as others have pointed out, one still needs to address the problem of including relevant non-register data. Often what's really needed is simply a package.

Many of the suggestions are fine as far as they go. The real problem is that C[++] is fundamentally quite bad at this sort of thing, and it's usually only made tolerable with vendor-supplied extensions (e.g. for specific micros), and add-on tools.

Sign in to Reply



feepingcreatures

12/8/2011 9:20 AM EST

Unless there is some performance reason why you shouldn't, I'd say it's better to encapsulate your I/O devices inside a nice low-level driver with clean and comprehensible interfaces. Then the precise method you use to define your I/O device memory mapping is irrelevant; as long as you can get those few lines of (relatively) trivial code correct, the rest of your application simply doesn't go near the hardware registers.

Sign in to Reply



Dan_Saks

12/9/2011 1:24 AM EST

How is what you are proposing different from the class I showed?

Sign in to Reply



feepingcreatures

1/4/2012 8:27 AM EST

oops - you're right, I only just noticed you've done exactly that in the class definition.

I guess I was just suggesting you can do exactly the same thing in C. And if you do that, it doesn't really matter how you access the registers (macros, structs, whatever); your application never sees any of it.

Sign in to Reply



Dan_Saks

1/5/2012 8:04 AM EST

Except that it's difficult to do exactly the same thing in C and C++. A C version that's just as abstract as the corresponding C++ will rarely be as efficient as C++, and likely be less efficient. A C version that's just as efficient as C++ will likely be less abstract.

Sign in to Reply



David Brown

12/8/2011 4:02 PM EST

Don't use non-standard "packed" attributes or pragmas unless you have a very good reason for it. A better technique to control your packing is simply to add dummy bytes (or bits, or words, or whatever) to make your layout explicit. That keeps everything clear, and avoids any mistakes with alignments, etc.

I agree entirely about using static assertions to check that you've got the structure right. You don't need checks on the individual offsets - if you've included explicit dummies, then it's enough to check the size or the offset of the last member. If that's correct, then so is everything else.

If your compiler supports it, use a "-Wpadded" flag to check that you haven't missed any pad bytes.

Sign in to Reply



Dan_Saks

12/9/2011 1:34 AM EST

I agree that it's better to stick with standard language features if you can. I usually point that out, but forgot to this time. Thanks for the reminder.

Sign in to Reply



David Brown

12/9/2011 2:11 AM EST

I don't have any problem with non-standard features where they are useful - you can't do embedded programming at all without using at least some non-standard features, and often they make the code significantly more efficient. But in this case, explicit padding is better than "packed" pragmas or attributes - being standard C is mostly just a bonus as far as I am concerned.

Sign in to Reply



DutchUncle

12/9/2011 9:11 AM EST

I dislike the x_enable() and x_disable() approach (using C) because the state is often reflecting a variable (particularly true for outputs, maybe not so much for the timer example). On the other hand, that means an if statement for every operation. In 40-year-old IBM assembler, I could have written a macro that tested whether the specified parameter was a known constant and generated either (a) only the single fixed line required or (b) the function call to test the value. C can't do that. I haven't gotten C++ to do it either. It is disturbing that the linua franca of current practice is nowhere near the best possible and does not encourage the best practices.

Sign in to Reply



DutchUncle

12/9/2011 9:14 AM EST

Another thought - Many of the definitions for hardware devices come in header files from the hardware vendors. They want to keep things very simple and very transportable to the oldest C compilers, and besides they don't care about complicated software constructs anyway, so they tend to do the simple one-at-a-time declarations. I could change them, but my manager would wonder why I wasted my time. We all should be leaning on the vendors to provide a better starting point.

Sign in to Reply



David Brown

12/9/2011 6:44 PM EST

Could you give an example of this? Common experience is that assembly needs complicated macros to generate different code depending on the parameters (or features of the parameters, such as whether or not they are constant).

Decent C or C++ compilers have no problem generating optimal code for constant values - you just have to make sure the function definition is known at compile time (typically an inline function), and you are not crippling your compiler by not enabling optimisation.

Sign in to Reply



Luis Sanchez

12/12/2011 3:59 PM EST

Very interesting! But I have to investigate because I don’t understand how a class makes it more difficult to change the registers than a C structure. Which part of the system makes it more difficult? If you make a direct access to the register location then the data will be corrupted regardless if you use a class or a structure. I’d appreciate it if you can talk more on this interesting topic.

Sign in to Reply



bartekbosowiec

12/13/2011 11:38 AM EST

With C struct you can't prohibit access to members.

Sign in to Reply



krwada

12/12/2011 7:00 PM EST

This is a great article. Unfortunately, the majority of firmware engineers rely on the MACRO substitution technique. They tell me that function and class encapsulation is too slow. Too slow??? In this modern age of multi-mega-mips processors and gobs of memory, it is much better to use function and object encapsulation. It is much safer and less error-prone than the MACRO substitutions.

Sign in to Reply



David Brown

12/16/2011 4:44 AM EST

People who still use function-like macros should learn about "static inline" (or inline C++ methods if you prefer) - you can define your functions with proper syntax, type checking, etc., and the compiled code will be as small and fast as you can get with macros.

Sign in to Reply



krwada

12/21/2011 11:55 AM EST

Inline or static inline is a great construct. The net result is very fast code that is checked during the build phase. Unfortunately, only C++ compilers have this construct. I am not aware of any C compilers with this feature.

Sign in to Reply



_DAN_

12/21/2011 4:43 PM EST

Ken,

Inline functions are part of C99, although many compilers supported them even before this.

(Then again, many shops still aren't using C99, so I can understand why you'd think that.)

Sign in to Reply



yermoungder

1/16/2012 10:40 AM EST

Presumably you deliberately selected to demonstrate how powerful compiler time asserts are by including an assertopm that would fail... ;-)

Sign in to Reply



yermoungder

1/16/2012 3:38 PM EST

In case you haven't got it yet...

0x12 != 12

But it does highlight how easy mixing up bases is in C and how you really need to mitigate by using assertions...or using another language!

Sign in to Reply



Dan_Saks

1/16/2012 6:57 PM EST

That 0x12 should have been just 12. Thanks for catching the typo.

Sign in to Reply



Please sign in to post comment

Navigate to related information

Datasheets.com Parts Search

185 million searchable parts
(please enter a part number or hit search to begin)
Featured Job On
Scroll for More Jobs