tag 标签: memory-mapped

相关博文
  • 热度 12
    2013-6-21 18:46
    3223 次阅读|
    0 个评论
    For some time now, I've been discussing how to implement and use virtual functions in C. , The basic approach is to implement each class as a C structure and implement each associated method as a function whose first parameter is a pointer to the structure. For each polymorphic class (a class with at least one virtual function), the corresponding C structure has a member called a vptr , which points to a table of function pointers called a vtbl . Last April, I showed how to mimic class inheritance in C in two distinct ways, which I call "inheritance by composition" and "inheritance by copying and editing." The two approaches differ in how they declare the vptrs and vtbls. I'm planning to show other variations, but not just yet. Rather, I want to pause and reflect on why I'm covering this topic. A few years ago, I wrote a series of columns on representing memory-mapped devices as objects in C and in C++. , Over the following year or so, I elaborated on the techniques, introducing other C++ features as appropriate. A few readers objected to my C++ implementations for failing to use virtual functions. This issue came up repeatedly, so I decided to address virtual functions in depth. My goal in writing this series was to explain what virtual functions are and how typical C++ compilers implement them. Most embedded.com readers use C rather than C++. Thus, I thought the best way to show how C++ typically implements virtual functions is to show how to implement virtual functions in C to get functionality and performance very similar to what you get from C++. I figured those of you already using C++ might learn more about how virtual functions work, and those of you using C might learn some techniques you can use in your code. I was also hoping to help some C users realise that C++ really is better for object-oriented programming. If you program in C, but you also want to do object-oriented programming, you probably should be using C++ instead of C. Yes, I know, some of you have constraints that prevent you from using C++. Some of you program on platforms for which no C++ compiler exists, or the available compilers don't mesh well with other tools you're using. In that case, you can approximate object-oriented features in C using techniques like the ones I've been describing. But don't kid yourself. Compared to C++, virtual functions in C are cumbersome and error-prone, and rarely offer any performance advantage. Indeed, C has a few features that C++ lacks, such as the restrict qualifier, which facilitates certain optimisations. But if you need these features, and your C++ compiler doesn't support them as extensions, you can write selected portions of your code in C. Needing these features is a poor reason to use C for everything, when C++ is so much better than C for object-oriented programming (as well as other things). Next time, I'll continue discussing the details of implementing virtual functions in C, not only because it's useful to know, but also because it touches on various other interesting programming topics.   References 1. Saks, Dan, "Implementing virtual functions in C++," Eetindia.com, April 2012. 2. Saks, Dan, "Implementing virtual functions in C," Eetindia.com, August 2012. http://forum.eetindia.co.in/BLOG_ARTICLE_13544.HTM. 3. Saks, Dan, "Alternative ways to mimic inheritance in C," Eetindia.com, April 8, 2013. http://forum.eetindia.co.in/BLOG_ARTICLE_16744.HTM. 4. Saks, Dan, "Accessing memory-mapped classes directly", Eetindia.com , March 21, 2011. http://forum.eetindia.co.in/BLOG_ARTICLE_7042.HTM. 5. Saks, Dan, "Memory-mapped devices as C++ classes", Eetindia.com , March 24, 2011. http://forum.eetindia.co.in/BLOG_ARTICLE_7022.HTM. 6. Saks, Dan. "Employ member new to map devices," Eetindia.com, November 25, 2011. http://forum.eetindia.co.in/BLOG_ARTICLE_10437.HTM. 7. Saks, Dan. "Unexpected trends," Embedded System Design, May 2, 2012. www.embedded.com/4372180.  
  • 热度 28
    2012-4-7 11:12
    2371 次阅读|
    0 个评论
    Over the past few years, I've written several articles about representing and manipulating memory-mapped devices in C and C++. Some readers commented that the C++ techniques I presented were lacking in that they failed to use more advanced features such as inheritance and virtual functions. Classes with virtual functions can be very useful, but they aren't the solution to every problem. Classes that represent memory-mapped devices, such as the ones I've presented, work with real hardware specifically because they don't use virtual functions. Last month (" Discriminated unions ," March 2012), I looked at the sort of problem that virtual functions are good at solving. I described a typical C solution using discriminated unions and discussed the limitations of that approach. This month, I'll explain how to use virtual functions in C++ to solve the same problem. In the not-too-distant future, I'll also show you how C++ typically implements virtual functions by showing how you can emulate them in C. Although my primary focus here is virtual functions, of necessity I'll discuss class derivation as well. However, I'll cover only those aspects of derivation that I need to explain virtual functions. Deriving a class The problem I presented last month involved representing a collection of two-dimensional geometric shapes such as circles, rectangles, and triangles. Each shape object contains some linear or angular distances sufficient to characterise the physical extent of the shape. For example, a circle has a radius, a rectangle has a height and a width, and a triangle has two sides and an angle. Each shape also had common attributes, such as position (planar coordinates), or outline and fill colours. In C++, you can represent the various shapes as a collection of classes. You start by defining a shape class which captures the properties common to all shapes: class shape { public: shape(); // constructor double area() const; double perimeter() const; ~~~ private: coordinates position; colour outline, fill; }; The class declares several functions as public members, including area and perimeter. The intent is that every shape—whether it's a circle, rectangle, triangle, or anything else—will provide these functions. In effect, class shape defines a common interface shared by every specific type of shape. The shape class also declares data members position, outline, and fill. The intent is that every shape object—no matter what kind of shape it is—will contain storage for these members. Now you can define classes for each specific shape by deriving them from class shape. For example, you can define the circle class as: class circle: public shape { public: circle(double r); // constructor double area() const; double perimeter() const; ~~~ private: double radius; }; In the class definition above, the class heading: class circle: public shape specifies that class circle is derived from class shape. The "derived from" class is commonly called the "base" class. The keyword public in the class heading indicates that the derivation is public. C++ also permits private and protected derivation, but I'm going to ignore those variants here, and make the simplifying assumption that all derivation is public. It's by far the most common usage. A derived class inherits all the data members of its base class. That is, the data members declared in the base class occupy storage in the derived class as well. Members declared private in the base class won't be accessible in the derived class, but they will still occupy storage in derived class objects. For example, the base class shape has three data members, declared as: coordinates position; colour outline, fill; The derived class circle declares one more data member: double radius; Thus, the data storage for a circle object is essentially the same as that of a structure defined as: struct circle { coordinates position; colour outline, fill; double radius; }; A derived class also inherits member functions from its base class. However, special member functions such as constructors, destructors, and copy assignment operators aren't inherited. If the circle class didn't declare area and perimeter as member functions, circle would inherit the functions exactly as they're defined in the shape base class. For example, I have yet to show the definition for shape's area functions, but whatever it is, it can't be right for circle. The problem is that a circle's area depends on its radius, but the radius data member is defined in class circle, not in class shape. Although a derived class may have access to data members inherited from a base class, a base class normally can't see the members declared in its derived classes. Thus, the shape class doesn't know that a circle has a radius. The definition for the circle's area function is very simple: double circle::area() const { return pi * radius * radius; } where pi is presumably a previously-defined constant representing Π. The keyword const appearing after the function parameter list indicates that area function treats each circle as a constant object. That is, computing the area of a circle doesn't alter the circle. A hierarchy of shapes You can easily create derived classes for additional shapes. For example, here are class definitions for rectangle: class rectangle: public shape { public: rectangle(double h, double w); double area() const; double perimeter() const; ~~~ private: double height, width; }; and for triangle: class triangle: public shape { public: triangle(double s1, double s2, double a); double area() const; double perimeter() const; ~~~ private: double side1, side2, angle; }; All of these shapes can be viewed as a hierarchy, typically drawn with the base class at the top, shown in Figure 1.   The base class doesn't know that it has any derived classes. Each derived class knows of its base class, but doesn't know about any of the other derived classes.  
  • 热度 16
    2012-3-11 11:50
    1587 次阅读|
    0 个评论
    Off and on for about two years, I've been writing about techniques for representing and manipulating memory-mapped devices in C and C++. My more recent columns have been more about C++ than C, focusing on language features such as constructors and new-expressions, which C++ doesn't share with C. 1, 2 Some readers have suggested that the C code I presented is preferable to the C++ code because the C structure implementations for devices are generally simpler than their corresponding C++ class implementations. Last month, I argued that the C++ implementations are actually better because they're easier to use correctly and harder to use incorrectly. Other readers have claimed that my C++ implementations are flawed because they're too simple—they don't use inheritance and preclude the use of virtual functions. Classes with virtual functions can be very useful, but they aren't the solution to every problem. Classes that represent memory-mapped devices, such as the ones I've presented, work with real hardware specifically because they don't use virtual functions. The new C++ Standard acknowledges the usefulness of such classes by defining categories such as standard layout classes , which avoid features such as virtual functions. In the coming months, I'll explain what virtual functions are. I'll show why they can be useful in some applications, but undesirable in classes that represent memory-mapped device registers. I'll also show you how C++ typically implements virtual functions by showing how you can emulate them in C. I'll begin this month by looking at the sort of problem that virtual functions are good at solving. I'll show a typical C solution using a construct called a discriminated union, and examine its limitations. An illustrative problem Suppose you have an application that employs two-dimensional geometric shapes, such as circles, rectangles, and triangles. At a minimum, each shape object contains some linear or angular distances sufficient to characterize the physical extent of the shape. For example, a circle has a radius, a rectangle has a height and a width, and a triangle has two sides and an angle. The shapes may have common attributes as well, such a position (planar coordinates), or outline and fill colors. A fairly traditional C implementation for a shape is a structure with a nested union, such as: typedef struct shape shape; struct shape { coordinates position; color outline, fill; shape_kind kind; union { circle_part circle; rectangle_part rectangle; triangle_part triangle; } u; }; The union member, u, is large enough to hold the largest of its members, but it can only store the value of one member at a time. In this example, each union member has a different structure type. For a circle, you need to store only the radius, as in: typedef struct circle_part circle_part; struct circle_part { double radius; }; For a rectangle, you need the height and width: typedef struct rectangle_part rectangle_part; struct rectangle_part { double height, width; }; For a triangle, you need two sides and an adjacent angle: typedef struct triangle_part triangle_part; struct triangle_part { double side1, side2, angle; }; When the union members have such simple types, you might find it easier to dispense with the named structure types and simply define unnamed structures inside the union, as in: union { struct { double radius; } circle; struct { double height, width; } rectangle; struct { double side1, side2, angle; } triangle; } u; The value of the shape's kind member indicates which union member is currently in use. The shape_kind type enumerates the possible values: enum shape_kind { sk_circle, sk_rectangle, sk_triangle }; typedef enum shape_kind shape_kind; For example, setting a shape's kind member to sk_rectangle indicates that it's now OK to access (write to or read from) the union's rectangle member and its height and width members.    
  • 热度 14
    2011-12-18 17:36
    1501 次阅读|
    0 个评论
    Several months ago, I wrote an article detailing 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.  
  • 热度 11
    2011-12-6 10:35
    1533 次阅读|
    0 个评论
    I have previously written a number of columns detailing alternative techniques for representing and manipulating memory-mapped devices in C and C++. Due to space limitations, the initial articles left a lot of details unresolved. 1 , 2 , 3 Most of the columns I've written since have focused on filling in the missing details, including a detour into constructors and automatic initialisation. 4 , 5 Classes typically use constructors to perform object initialisation. Classes for memory-mapped devices should be no different. However, as I explained in August, many common declarations for memory-mapped objects don't invoke constructors implicitly. 6 In my most recent column, I explained how you can use new with placement in C++ to invoke constructors explicitly. 7 Explicit constructor calls can be very useful when you need to control initialisation order precisely. However, implicit constructor calls should still be the norm because they provide guaranteed initialisation. This month, I'll show you how to provide guaranteed initialisation for memory-mapped devices by defining operator new as a class member. Where we were For my examples, I've been using a class that represents a programmable timer. The class definition looks like: typedef uint32_t volatile device_register; class timer_type { public: enum { TICKS_PER_SEC = 50000000 }; typedef uint32_t count_type; timer_type() { disable(); } void disable() { TMOD = ~TE; } void enable() { TMOD |= TE; } void set(count_type c) { ... } count_type get() const { ... } private: enum { TE = 0x01 }; device_register TMOD; device_register TDATA; device_register TCNT; }; The class has private data members that represent the timer's device registers, along with public member functions that provide a modest assortment of basic timer operations. One of those operations is a default constructor (highlighted above in red). The Standard C++ Library provides a placement form of operator new declared in the standard header as: void *operator new(std::size_t, void *p) throw (); Calling this function does nothing but return the value of its second argument. Programs can use this placement operator new to construct an object at a particular address. For example, you declare a timer object as: extern timer_type the_timer; and use the linker to place the object in memory. Then you can apply the constructor via the placement new-expression: new (the_timer) timer_type; (I explained the placement new syntax in my previous column. 7 ) If you define the timer using a constant pointer instead: timer_type *const the_timer = reinterpret_cast(0xFFFF6000); then you can apply the constructor via the placement new-expression: new (the_timer) timer_type; Using placement new doesn't provide guaranteed initialisation. By declaring a memory-mapped object and then initializing it using placement new , you can inadvertently do the first step without the second. Fortunately, you can get guaranteed initialisation by using member operator new to combine the steps. Operator new as a class member In applications that use dynamic memory, it often happens that the majority of dynamically-allocated objects are of just a few types. When that is the case, you may be able to achieve significant performance improvements by using special-purpose allocation and deallocation functions for just those few heavily-used types, while still using the library's general-purpose allocation and deallocation functions for all the other (less used) types. C++ lets you implement special-purpose memory managers for objects of a particular class by defining operators new and delete as members of that class, as in: class widget { public: void *operator new(std::size_t n); void operator delete(void *p) throw (); ... }; Thereafter, a new-expression such as pw = new widget; allocates memory using widget::operator new rather than the global operator new . Likewise, delete pw; deallocates memory using widget::operator delete . You can use a member operator new to place and automatically construct a memory-mapped object, as in the timer_type class shown below: class timer_type { public: enum { TICKS_PER_SEC = 50000000 }; typedef uint32_t count_type; void *operator new(std::size_t) { return reinterpret_cast (0xFFFF6000); } timer_type() { disable(); } ~~~ }; This operator new behaves like (global) placement new in that it places the object at a specified address. However, a new-expression that calls this member new doesn't use the placement syntax. You can invoke this member operator new by using an ordinary-looking new-expression, such as: timer_type *const the_timer = new timer_type; This new-expression uses the timer_type 's operator new to place the timer_type object in its memory-mapped location, and uses the timer_type 's default constructor to initialise the object automatically. Pretty neat, huh? Name lookup In addition to providing guaranteed initialisation, using a member operator new instead of placement new avoids a potential mishap. Once again, the Standard C++ Library provides a placement form of operator new declared as: void *operator new(std::size_t, void *p) throw (); Calling this function does nothing but return the value of its second parameter. As I explained in my previous column, most C++ implementations define it as an inline function: inline void *operator new(std::size_t, void *p) throw () { return p; }