Volatile Objects: Programming Pointers
Volatile Objects: Programming Pointers
PROGRAMMING POINTERS
Dan Saks
Volatile Objects
For the past few months, Ive been
discussing the const qualifier, mostly with an eye on using const to place objects into ROM. I havent said all I have to say about const, but part of what I have left involves the volatile qualifier, as well. So this month, Ill introduce you to the volatile qualifier. The volatile qualifier can appear anywhere that the const qualifier can. Whereas const declares objects that the program cant change, volatile declares objects whose values might be changed by events outside the programs control. A typical example of a volatile object is a memory-mapped input/output (I/O) port. My focus here is on explaining the volatile qualifier rather than the details of I/O. To keep things simple, the remaining discussion uses only the memory-mapped model. A typical hardware device often communicates through a sequence of device registers. Some registers communicate control and status informaOne common technique for manipulating control/status registers is to use symbolic constants to represent masks for isolating bits, and use bitwise operators (like & and |) to set, clear, and test bits in registers. For example, you might represent a 16-bit control/status register as:
typedef short int control;
The volatile qualifier declares objects whose value can be changed by events beyond the programs control. Volatile objects are useful for memory-mapped I/O ports.
tion, while others communicate data. A given device may use separate registers for input and output. Registers may occupy bytes, words, or whatever the architecture demands. The simplest representation for a data register is as an object of the appropriate size integer type. For example, you might declare a one-byte register as a char or a two-byte register as a short int. Then you can move data to a memory-mapped device by storing a value into its output data register, and retrieve data from that device by reading from its input data register. A control/status register is not really an integer-valued objectits a collection of bits. One bit may indicate that the device is ready to perform an operation, while another might indicate whether interrupts have been enabled for that device. A device might not use all the available bits in its control/status register.
#define ENABLE 0x40 /* enable interrupt */ #define READY 0x80 /* device is ready */
Memory-mapped I/O
Most modern computers communicate with I/O devices using either memory-mapped I/O or port I/O. Memory-mapped I/O maps device registers into the conventional data space. To a C or C++ programmer, memory-mapped I/O registers look more or less like ordinary objects. That is, storing into a memorymapped device register sends commands or data to a device; reading from a memory-mapped I/O register retrieves status or data from a device. This is the approach used in the Motorola 68K family of processors. In contrast, port I/O maps control and data registers into a separate (often small) data space. Port I/O is similar to memory-mapped I/O except that programs must use special instructions, such as the in and out instructions on the Intel x86 processors, to move data to or from the device registers.
which uses a cast expression to initialize pc to point to the address of a memory-mapped control/status register at some fixed address. (Although C and C++ both allow cast expressions such as the one above that convert integers to pointers, the exact behavior of such casts varies across platforms.) Once pc points to a memorymapped control/status register, the program can communicate with the device by testing or setting the value of the register via pc. For example:
SEPTEMBER 1998
101
PROGRAMMING POINTERS
clears the control registers enable bit, so the device will not trigger interrupts. A loop such as:
while (*pc & READY == 0) /* do nothing until ready */;
repeatedly polls (tests) the control registers ready bit until that bit is non-zero. Many devices use control/status and data registers in tandem. The following declarations declare a bi-directional device (supporting both input and output) using a pair of registers for input and another pair for output:
typedef typedef #define #define short int control; short int data; ENABLE 0x40 READY 0x80
sends the value of character c to the output device. It immediately clears the ready bit in the corresponding output control/status register to indicate that the device is busy. The hardware automatically sets the ready bit when the device completes the current operation. That is, it sets the ready bit to indicate that the device is ready to start another operation. Thus, a loop such as:
while (p->out.c & READY == 0) ;
pio->out.c is actually a device register whose value could change due to some external event such as the completion of an I/O operation. The compilers optimizer might therefore conclude that either the ready bit in pio->out.c is always set, or the ready bit in pio->out.c is never set. In generating code, the compiler considers both possibilities. If the ready bit is always set, the program never enters this loop: while (pio->out.c & READY == 0) ;
waits until the output device is ready to receive another character. As a somewhat more complete example, a function such as:
void put(char const *s, ioport *p) { for (; *s != \0; ++s) { while (p->out.c & READY == 0) ; p->out.d = *s; } }
If the ready bit is never set, the program never leaves the loop. In either case, theres no need to test the condition more than once. The optimizer transforms the loop into:
if (pio->out.c & READY == 0) for (;;) ;
typedef struct port port; struct port { control c; data d; }; typedef struct ioport ioport; struct ioport { port in, out; };
sends the characters in null-terminated string s to the output device controlled by *p.
which tests the condition only once, and then either loops forever or never loops at all. After this optimization, the driver code looks like:
if (pio->out.c & READY == 0) for (;;) ; pio->out.d = \r; if (pio->out.c & READY == 0) for (;;) ; pio->out.d = \n;
disables output interrupts for that I/O ports output port. An assignment such as:
pio->out.d = c; 102
SEPTEMBER 1998
Again, as far as the compiler can tell, the ready bit is either always set or never set. If its always set, then the program skips both loops. If its never set, the program falls into the first loop and never escapes. In either event, the program never executes the second loop. That loop is dead code, and the optimizer can eliminate it. After this optimization, the driver code looks like:
if (pio->out.c & READY == 0)
PROGRAMMING POINTERS
As far as the compiler can see, the second assignment overwrites the result of the first assignment. Therefore, only the last assignment is worth keeping. The final optimized code looks like:
if (pio->out.c & READY == 0) for (;;) ; pio->out.d = \n;
You had to hope you turned optimizations off in just the right places. The volatile qualifier eliminates the guesswork. It identifies objects, such as memory-mapped ports, that may be changed by events that compilers cannot detect. For example,
ioport volatile *const pio = (ioport *)0xFFA0;
optimizations only for accesses to volatile-qualified objects. It does not inhibit any other optimizations. An object can be both const and volatile. For example, given:
iport volatile const *pi = ...;
It does the wrong thing, but much more efficiently! The problem here is that a memory-mapped I/O port isnt an ordinary object in RAM. With an object in RAM, a compiler can assume that if the program places a value in the object, the value will remain in that object until the program places a different value there. Thus, a compiler can optimize away references to the object if the compiler can determine that the value hasnt changed since the last reference. With a memory-mapped port, a compiler cant make the same assumption. The value in a port may change spontaneously. If the program has an expression that looks at the ports value, the compiler must generate code that actually fetches the value. It cant optimize away those fetches.
declares that pio has type const pointer to a volatile ioport. Thus, *pio is an object whose value may change spontaneously. This tells the compiler that it shouldnt optimize away references to *pio, even if it appears that the value of *pio hasnt changed since the last reference. The volatile qualifier prevents
then *pi is a const volatile iport object. The program cant write to *pi, but it must act as if *pis value might change spontaneously. This is typical behavior for an input port. Of course, theres more to the volatile qualifier than what Ive covered in this brief discussion. Although I will resume my discussion of const in upcoming articles, I will fill in details on volatile as well. esp Dan Saks is the president of Saks & Associates, and a contributing editor for the C/C++ Users Journal. Write to him at [email protected].
103