Art Mansky has been involved in software development in C for the past ten years, primarily for microprocessor-based, realtime applications. He can be reached at Advanced Technology Department, Vitro Corporation, 14000 Georgia Ave., Silver Spring, MD 20906.
Introduction
This article discusses the use of the C preprocessor in low-level C programming. It illustrates the concepts involved with a small example, the control of a peripheral communications interface chip. One of the most useful features of C for device control programming is its ability to specify hardware addresses. A typical device control chip contains a number of control and status registers.Typically, there are byte-long registers accessed via the chip address plus an offset. For example, suppose that the address of a status register is offset by 2 and the address of a control register is offset by 4 from the chip address of 0x5e00. The code below will read the status register and clear the control register.
statval = *(unsigned char *)0x5e02; *(unsigned char *)0x5e04 = 0x00;Casting allows access to the register contents, via the register's address, as an eight-bit unsigned value.While the preceding code may do the job, it has a number of problems. If a hardware modification changes the chip's address, for example, all places in the code where its registers are accessed must be changed. Also, the casting operation quickly clutters the program if used repeatedly. These deficiencies can be remedied by using macros as illustrated later in this article.
Bit Manipulations
A bit is set (given a value of one) by performing a logical OR operation with a one bit. A bit is cleared (set to zero) by performing a logical AND with a zero bit. Bits within a register can be set or cleared by first constructing a "bit mask" and then performing the appropriate logical operation. Following are two examples.
/* clear high nibble (higher 4 bits) */ regval1 &= 0x0f; /* set low nibble (lower 4 bits) */ regval2 |= 0x0f;When ANDing, bits that are set to one in the mask will leave the bits in those positions in the destination unchanged. When ORing, bits that are set to zero in the mask will leave the corresponding destination bits unchanged.Note that this bit manipulation technique performs a read operation (to obtain the current value) as well as a write operation (to change the value). On many controller chips, when a particular address is accessed via a read operation, it provides a status byte. When that same address is written to, it serves as a control byte. These are separate and distinct functions. In this case, the bit modification techniques described above cannot be used, since a change to a register performs both a read and a write. In such cases, a C variable is set aside to hold the value written to the control register. This value can then be modified and the new value re-written to update the register itself.
/* clear upper nibble */ ctrlreg &= 0x0f; *(unsigned char *)0x5e04 = ctrlreg;Simple Boolean logic (such as this) and the ability of C to access specific hardware addresses, let you manipulate the bits in a chip's registers and thus control the chip's operation.
An Example Chip
To illustrate the use of the C preprocessor's macro facility for device control, I will write some sample code to control an actual chip. One of the most useful pieces of device control hardware is a multi-function input/output controller, typically containing timers, interrupt controllers, and a Universal Synchronous/Asynchronous Receiver-Transmitter (USART). Such a chip performs a variety of functions. For example, it might generate vectored interrupts based on I/O conditions, provide handshaking interrupt lines, generate periodic timed interrupts, measure elapsed time, supply baud rate clocks for serial communication, and support a serial I/O channel.The Motorola MC68901 Multi-Function Peripheral (MFP) is a member of the 68000 family of peripherals. The MFP has eight individually programmable I/O pins with interrupt capability, a 16-source interrupt controller, four timers, and a single-channel full-duplex USART. Each of the 24 registers is directly addressable via five register-select lines. Combined with the MFP chip-select line, these form the unique address that allows access to each register.
Because the MFP is so versatile, I will use only a small portion of its capabilities in the examples. In particular, I won't use its USART. In the examples, I assume that I have a 2 MHz timer clock input to the MFP and that three hardware devices (numbered 1 through 3) are connected to the general purpose input/output register at bits 3, 0, and 7, respectively.
Definitions And Macros
The file mfp_defs.h, shown in Listing 1, contains the definitions for the MFP register addresses. I have also included the definitions that specify how the three devices are connected to the I/O lines. Only the definition for MFP, the chip address itself, is a true fixed hardware address. Its register addresses are all defined as offsets from that. How the chip select pins are connected on the address bus to the microprocessor CPU determines the MFP address. By defining the register addresses using the MFP address, a simple one-line change (to the MFP definition) will correctly adjust the address definitions for all of the registers.Listing 2 shows the file mfp_macs.h, which holds the definitions for the macros. The first macro, REGVAL, performs the cast operation used to access a register's contents. The next two macros construct the masks for turning a single bit on or off. The bitnum parameter should be a number from 0 to 7, with 0 being the least significant bit in the byte and 7 the most significant. BITON_MASK creates a mask for turning a bit on, and so creates a byte with a one bit in the desired position and all zero bits elsewhere. This will then be ORed into the destination to turn on the bit in that position. BITOFF_MASK creates a mask for turning a bit off, and so creates a byte with a zero bit in the desired position and all one bits elsewhere. This will then be ANDed into the destination to turn off the bit in that position.
The next two macros in Listing 2, SET_BITS and CLR_BITS, are the bit-wise OR and AND of a given bit mask (such as one created by one of the previous two macros) into a destination location. The last two macros combine the concepts of the previous ones, to create a pair of bit-set and bit-clear macros. Each takes a byte address and sets or clears the requested bit in that byte.
Let's examine some example C routines. (Although each is written as a separate routine, the code that forms the routine body would most likely appear in real life as a segment of code in a larger routine.) In the first example, shown in Listing 3, I want to clear a bit in the general purpose data direction register. This specifies that the corresponding line in the general purpose I/O register will now be an input line. For this example, we assume that bit 3 is the line in the GPIO that we want to be an input line (connected to a hypothetical device number 1) so bit 3 is the bit that we must clear in the data direction register. To emphasize how large a role personal style plays, I have coded this task in four ways. Although each of the four lines of the dev_input routine looks different, each compiles to the same assembly language instruction. I could create a macro SET_DEV1_INPUT that takes no parameters and is defined as one of the four macros in the example (or any of a number of other equivalents). While macro use is encouraged, it can be carried too far.
In the second example, shown in Listing 4, I am working with the same register, but now I want to set three lines as outputs. I have performed this twice so that I can see alternatives. The first method is to use the existing macro for setting a bit three times in a row, once for each bit. In the second method, I have constructed the appropriate bit mask for setting all three and just use the "set bits using a mask" macro. The first method results in less efficient object code. (Or maybe not. In some microprocessors, three "bit set" operations might be be quicker than one OR operation.) However, in general, creating a special bit mask for one particular situation may be a bad idea. Later, if there is a need to change which bits are manipulated, the new mask must be correctly determined. Considering the chances of getting the mask wrong in the first place, the best approach is the simple but slightly less efficient one.
In the example routine in Listing 5, I have a delay routine that uses timer B of the MFP. What is not shown here is the timer B interrupt service routine, which increments the global variable stimer and returns. We want a routine that takes as input the time to delay in units of seconds. However, the timer clock input is a 2MHz clock. Since the timer countdown is only an eight-bit value, we combine it with the MFP timer's built-in ability to prescale the count to obtain a timed interrupt every one-hundredth of a second. (A prescale of 200 combined with a countdown of 100 results in the clock being divided by a factor of 20,000. 2MHz/20,000 yields 100Hz.) By comparing the stimer variable with 100 times the requested time value, we achieve the desired delay in seconds.
I use this delay routine in my final example, shown in Listing 6. The reset line for device number 3 is connected to the general purpose I/O register's bit 7. To ensure that the device is initialized, its reset line must be held high (value of 1) for at least 30 seconds. This routine sets the reset line high, delays for 30 seconds using the routine from the previous example, and drops the reset line low (value of 0). Note that through the use of macros and our delay subroutine, the code for this reset operation is very simple and readable. It clearly tells how the reset operation is done, without cluttering the code with all the details. Anyone interested in them can examine the constant definitions at the top of the routine and the constants and macros as defined in the included files mfp_defs. h and mfp_macs. h.
Conclusion
Deciding where and how to use the C preprocessor's macro facility may be a matter of individual style. However, not using macros will result in hard-coded constants and repeated small patterns of source code (such as the cast operation in the example). This source code is difficult to understand and maintain and has a cluttered appearance. These deficiencies often apply to C source code for applications that interact with hardware devices, applications that seem to require code that is less structured than that found in higher-level applications.As with any good tool, however, the macro can be overused. You can create a main routine that is just one macro. You would then need to examine the definition for that macro and all of the macros that it uses, and so on, to eventually see the actual C source statements. The most useful macros are often those that can be frequently applied in the source code. If a macro cannot be used in several places, you might consider removing it. Exceptions, of course, would include macros that are infrequently applied but greatly clarify the source code.
Device control software is often difficult to write, is not as well-structured as code for higher-level applications, and is hard to understand and maintain. Careful use of the C preprocessor when writing device control software aids in developing code that is well-structured, readable, and easy to maintain. Its use can make this frequently difficult software task easier, and more enjoyable, too.
Bibliography
Arnold, Ken. "Fun With the C Preprocessor," UNIX Review, April, 1988.Gehani, Narain. C: An Advanced Introduction, Computer Science Press, 1985.
Kernighan, Brian W. and Dennis M. Ritchie. The C Programming Language, Prentice-Hall, 1978.
Motorola. MC68901 Multi-Function Peripheral Data Sheet, 1984.