It's amazing what you can do with a single general-purpose I/O pin. Join us for a comprehensive look at the possibilities.
Every computer system must interact with the outside world if it is to be of use to anyone. General-purpose computers offer a wealth of choices for such interactions. A program may receive input from a mouse, keyboard, disk files, serial ports, or Ethernet. It may also send output to many of those same devices, as well as present information graphically on a printer or video display.
Embedded systems, however, are not often blessed with an abundance of peripheral devices. In some cases-particularly when first bringing up a new board-we may have nothing more than a few discrete bits with which to report what's going on inside the system, or to communicate our wishes to it. Fortunately, much can be done with a lone bit. In this article, I'll discuss the basics of general-purpose I/O pins, including programming considerations, and some simple circuitry for really putting them to work.
Even the most primitive processor provides pins that can be used to provide a single bit of input or output. You can also implement this capability using memory-mapped I/O devices, programmable logic (PLDs, FPGAs, and the like), or ASICs. The direction of an I/O pin may be fixed (for example, input-only or output-only), or may be selectable. To further complicate things, the pin may be shared by another device on the processor, such as a timer or serial port. You may be able to use the pin for digital I/O, or, for example, as the transmit signal for a serial port-but not both at the same time. Carefully read all the documentation about the pin you plan to use, and experiment with the actual hardware to be sure you understand how to control it.
Bit manipulation
Consider the registers in Figure 1 (and the definitions in Listing 1), which control the discrete I/O pins on an imaginary 8-bit processor. This processor has two parallel ports (Port A and Port B), and a timer. Each of these devices provides means to read and write one or more pins on the processor package. Each works a little differently, so let us consider them in turn.
Figure 1: Registers
Listing 1 Register definitions
|
#define PADR
|
((unsigned char *)
|
0xFFFF)
|
|
#define PBDR
|
((unsigned char *)
|
0xFFFE)
|
|
#define PBDDR
|
((unsigned char *)
|
0xFFFD)
|
|
#define TCR
|
((unsigned char *)
|
0xFFFC)
|
|
#define TCVR
|
((unsigned char *)
|
0xFFFB)
|
|
#define TCRR
|
((unsigned char *)
|
0xFFFA)
|
Port A is the simplest of the three. It is associated with eight pins on the processor; four are always inputs and four are always outputs. These pins are controlled by the Port A Data Register (PADR) at address 0xFFFF. Writing to any of the high four bits of the register (PAO0-PAO3) will drive the associated pin high or low. For example, we could set the PAO1 signal high and PAO0, PAO2, and PAO3 low by writing 0x20 to the register:
*PADR = 0x20;
Similarly, we could read the state of one of the input pins by looking at the corresponding low-order bit (PAI0-PAI3) of the same register:
if (*PADR & 0x08)
printf("Input PAI3 is high.\n");
else
printf("Input PAI3 is low.\n");
Like Port A, Port B is also associated with eight pins, but these are all bidirectional signals. The value of each pin is read or written by a bit in the Port B Data Register (PBDR), while the direction of each signal is controlled by a second register called the Port B Data Direction Register (PBDDR). If the value of a bit in the PBDDR is zero, the corresponding pin is an input; setting the bit to 1 configures it as an output. For example, to force the pin associated with bit 3 of Port B to a ground, we need to firstconfigure the pin as an output by setting bit 3 of the PBDDR. We can then clear bit 3 of the PBDR to drive the output:
// Configure bit 3 as output
*PBDDR |= 0x08;
// Force the output low
PBDR &= ~0x08;
Note that by using the |= and &= operators we can change only the bits we are interested in.
Likewise, to read the pin associated with bit 7, we would need to clear bit 7 of the PBDDR to configure it as an input, then read bit 7 of the PBDR to learn the pin's state:
*PBDDR &= ~0x80;
if (*PBDR & 0x80)
printf("The pin is high.\n");
Once a bit in the PBDDR has been set, we can leave it alone, and continue writing or reading the PBDR without changing the direction. Frequently, we will set the direction bits once at power-up, then never touch them again, since it is quite common for a pin to be used strictly as an input or an output during the lifetime of a particular program. (A notable exception is communicating with components using the I2C protocol, where a single pin may be configured as an output to clock a command out to the device, then configured as an input to read the response.)
Our imaginary device provides one final pin that we can control. This pin is associated with the Timer, and is controlled by the Timer Control Register (TCR). The TCR consists of the following control bits:
- SEL: This bit determines whether the pin is controlled by the timer (0) or is used as a general-purpose I/O controlled by the DIR, IN, and OUT bits. When SEL=0, the pin changes state every time the counter overflows.
- DIR: When SEL is 1, this bit determines whether the pin is configured as an input (0) or output (1). If SEL is 0, the DIR bit has no effect.
- IN: When SEL=1 and DIR=0, reading this bit returns the state of the pin. If SEL=0 or DIR=1, this bit returns an unpredictable value.
- OUT: When SEL=1 and DIR=1, setting/clearing this bit drives the pin high/low. If SEL=0 or DIR=0, setting this bit has no immediate effect.
- ENAB: Setting this bit to 1 enables the timer to start counting. When ENAB changes from 0 to 1, the Timer Counter Value Register (TCVR) is loaded from the Timer Counter Reload Register (TCRR), and increments every clock cycle. When the TCVR overflows, the OVER bit is set, the pin is toggled if SEL=0, and the TCVR is reloaded from the TCRR.
- OVER: This bit is set when the timer counter overflows. It is cleared by writing a 1 to it.
By setting the SEL bit in the TCR, we can use the pin for general-purpose I/O, using the DIR pin to control the direction, and setting or reading the pin state with the OUT and IN bits. When the SEL bit is low, the pin is controlled by the timer, toggling state whenever the timer overflows. The timer runs when the ENABLE bit of the TCR is set. When the timer overflows, the OVERFLOW bit of the TCR is set, and the timer is reloaded from the Timer Load register.
Using the timer pin as a general-purpose output would require: setting the SEL bit in the TCR, to specify that the pin should be used for general-purpose I/O; setting the DIR bit; setting the direction to output; and setting the OUT bit to the desired state. The code would look like this:
// Set the output to 0
*TCVR &= ~0x10;
// Set pin direction to Output
*TCVR |= 0x40;
// Use the timer pin for general-
// purpose I/O
*TCVR |= 0x80;