原创 The troubles with constructing memory-mapped objects

2011-8-3 00:48 2956 14 14 分类: 消费电子

A few months ago, I wrote a column explaining some common approaches to representing and manipulating memory-mapped devices in C.1 I followed that with another column explaining some better alternatives using classes in C++.2 The C++ alternatives are better in the sense that they yield interfaces that are typically easier to use correctly and harder to use incorrectly than the C alternatives, and yet have much the same performance.


In C, structures often provide the best way to model the device registers of memory-mapped devices. For example:



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


defines the layout for a timer that employs three device registers.


The header file that defines this structure might also define useful constants and types for manipulating the registers, such as:



#define TE 0x01
#define TICKS_PER_SEC 50000000


typedef uint32_t timer_count_type;
along with functions that provide basic operations for programming a timer, such as:



void timer_disable(timer_type *t);
void timer_enable(timer_type *t);
void timer_set(timer_type *t, timer_count_type c);
timer_count_type timer_get(timer_type const *t);


In C++, you can wrap all of the timer components into a single class that more effectively hides some of the timer's complexity. The class definition looks something 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;
};


As I explained last April, a constructor is a special class member function that provides guaranteed initialization for objects of its class type.3 This timer_type class doesn't have any constructors, but it probably should.


This month, I'll discuss adding constructors to classes that represent memory-mapped devices. As you'll see, writing such constructors is no big thing. The challenging part is getting the constructors to execute.


Defining constructors
In many embedded systems, the appropriate way to initialize a device is to put it into an inactive state. In such systems, the constructor for a timer might simply make sure that the timer is disabled. To do this, simply add the constructor declaration to the class definition:



class timer_type
{
public:
~~~
timer_type();
~~~
};


and define the function as:



inline
timer_type::timer_type()
{
disable();
}


Alternatively, you can define the function within the class definition, as in:



class timer_type
{
public:
~~~
timer_type()
{
disable();
}
~~~
};


in which case the function is also implicitly an inline function.


Declaring objects
Normally, you don't choose the memory locations where program objects reside—the compiler does, often with substantial help from the linker. For example, if the compiler encountered an object declaration at global scope such as:



timer_type the_timer;


the compiler would set aside so many bytes at some offset within some code segment. If that definition appeared at local scope, the compiler would set aside so many bytes at some offset within the stack frame of the function containing the definition. As I a few months ago, in either case, the compiler would automatically plant code to invoke the constructor in the "right" place.4


However, a timer—or any object representing a memory-mapped device—isn't an ordinary object. The compiler doesn't get to choose where the object resides—the hardware designer does. Thus, to access the object, the code needs a declaration for a name it can use to reference the memory-mapped location as if that location were an object of the proper device type. As I explained last year, that declaration can have different forms.


Failure to launch
With most C and C++ compilers, you can name a memory-mapped object using a standard extern declaration such as:



extern timer_type the_timer;


and then use linker command options or linker scripts to force the_timer into the desired address. However, this declaration is not a definition, so the compiler doesn't generate a constructor call for the object.


That last sentence is worth elaborating. An object declaration is a statement that effectively says to the compiler: "Here's a name and some attributes for an object that's somewhere in this program, possibly here." An object definition is a statement that says: "Here's a name and the complete set of attributes for an object that's right here."


All definitions are declarations, but not all declarations are definitions. An object definition prompts the compiler to generate code to allocate the object. A non-defining declaration does not. A definition for an object of class type with constructors also prompts the compiler to generate code to call a constructor. A non-defining declaration does not.


Some C and C++ compilers provide a non-standard language extension that lets you position an object at a specified memory address. For example, to declare a timer object residing at location 0xFFFF6000, you might write a memory-mapped object declaration of the form:



timer_type the_timer @ 0xFFFF6000;    


with one compiler, or:



timer_type the_timer _at(0xFFFF6000);


with another. With most compilers that support such declarations, these aren't definitions either. When that's the case, the compiler won't generate a constructor for these declarations.


The other common alternative is to define a pointer to a device register as a macro:



#define the_timer ((timer_type *)0xFFFF6000)


or as a constant pointer:



timer_type *const the_timer
= (timer_type *)0xFFFF6000;


In C++, using a reinterpret_cast operator, as in:



timer_type *const the_timer
= reinterpret_cast(0xFFFF6000);


reduces the hazard of the cast somewhat. You can also use a reference instead of a constant pointer, as in:



timer_type &the_timer
= *reinterpret_cast(0xFFFF6000);


These declarations for the_timer as a pointer (or reference) are object definitions, but they define only the pointer (or reference) to a memory-mapped object. They don't define the memory-mapped object itself. Thus, once again, the compiler won't generate a constructor call applied to the memory-mapped object.


Of course, C programmers don't face this issue. C doesn't provide constructors, so C programmers just write named initialization functions and call them explicitly.


C++ programs could do this, too. For example, you could define an initialization function for timer_type, as in:



class timer_type
{
public:
~~~
void construct() { disable(); }
~~~
};


Then, you could write:



timer_type &the_timer
= *reinterpret_cast(0xFFFF6000);
the_timer.construct();


to set up a timer and initialize it. And there'd be no problem if it weren't so darned easy to forget to write such calls now and then.


Stay tuned
Constructors serve a useful purpose, and it would be preferable to use them whenever possible to initialize memory-mapped objects. Fortunately, C++ provides alternative forms for operator new that make this feasible. That will be my subject next month.


Endnotes:
1. Saks, Dan, "Alternative models for memory-mapped devices", Eetasia.com, March 2011.http://forum.eetasia.com/BLOG_ARTICLE_6978.HTM.
2. Saks, Dan, "Memory-mapped devices as C++ classes", Eetasia.com, March 2011. http://forum.eetasia.com/BLOG_ARTICLE_7018.HTM.
3. Saks, Dan, "Demystifying constructors," Eetasia.com, April 2011. http://forum.eetasia.com/BLOG_ARTICLE_7112.HTM
4. Saks, Dan. "Constructors and object definitions," Eetasia.com, April 2011. http://forum.eetasia.com/BLOG_ARTICLE_7322.HTM
 

文章评论0条评论)

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