Twiddle Bits - Embedded.com

Twiddle Bits

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;

Note the order in which the bits are set. First the output bit is set to the desired value, then the direction is set to output, and finally the pin is configured for general-purpose I/O. This will minimize the number of transitions of the output pin as the registers are being configured. Of course, if we don't care about that, we can set everything up in one line of C:

*TCVR = (*TCVR & ~ 0x10)
    | (0x80 | 0x40);

though this may not produce smaller or faster object code.

None of this code should be used if we may ever care about the state of the OVER bit in the TCVR-it will be cleared whenever we change any of the other bits. While a C statement such as:

*TCVR |= 0x80;

looks like a single operation, it really consists of three parts: reading the current value of *TCVR; modifying the value just read; and writing the new value back to *TCVR. Thus, this type of instruction is sometimes known as a “read-modify-write” operation. Generally, this is good to do when manipulating individual bits, because it allows us to change only the bit or bits that interest us, without affecting bits that other parts of the software may care about. Unfortunately, it doesn't have quite the desired effect in this case. Consider what happens when the OVER bit in the TCVR is set when we try to turn on the output bit by executing *TCVR |= 0x10 . First, the TCVR is read, with the OVER bit set to 1. Then, this value is bitwise ORed with 0x10, turning on the OUT bit and leaving the OVER and other bits unchanged. Finally, the new value is written back to the TCVR. But the OVER status bit is cleared by writing a 1 to it. By taking pains to avoid disturbing other bits, we've actually caused the very problem we were trying to avoid.

We can work around this problem by always masking out the OVER bit before writing to the TCVR. For example, *TCVR |= 0x80 ; could be re-written as:

*TCVR = (*TCVR & ~ 0x04) | 0x80;[1]

While this works, it is not as clear or concise as the original (incorrect) code. This is a good argument for using functions or macros to access individual bits within a register, especially when the registers are used by different parts of a large program. Can you imagine how troublesome it would be to track down and correct each reference to a register across all the files of a large embedded system?

You must understand how to control your hardware before you get too carried away writing code. With programmable logic, you must also anticipate how that logic might change in the future.

Shadow registers

Programmable logic can present unusual challenges. For example, someone could create a register in an FPGA that, when written, would set the values on a set of (output) pins on the device, but when read would report the values present on a different set of (input) pins. In this case, any read-modify-write operation to set an output bit would produce unpredictable results. The best way to deal with this situation would be to maintain a shadow register, a local or global variable to keep track of what values you've written to the output register.

Now that you can use input and output bits, let's put them to work. Of course, doing anything really useful requires additional hardware, but even an unattached bit can be a powerful tool. Later, we'll look at how to light an LED, but for now let's see what can be done without hardware support.

Outputs

First, we'll need a way to examine the state of the pin. For very basic tasks, such as verifying that we can set the output high and low, a handheld multimeter is adequate, but for almost anything else, you'll want an oscilloscope.[2]

At this point, let us assume that you've found an output pin you want to use, figured out how to control it, and written the functions SetHigh() , SetLow() , and Toggle() to control it. These functions can be useful from the start of your project. If you're bringing up a new system, you'll first want to validate your tool chain by compiling, loading, and running a simple program. If you're confident-and extremely lucky-you might go straight to printing “Hello, world!” over a serial port or Ethernet. If it works, you'll be off to a good start, but if it doesn't it may be a lot of work to figure out where things went wrong. It is much simpler to start with a simple program to “tickle” the output bit in a way that will let you know your code is being executed (especially if you don't have a trustworthy debugger). For example, you could start with something like the following:

void main()
{
    while (TRUE) {
        Toggle();
    }
}

and look for a square wave on the output pin. If it's there, you've demonstrated that the processor will boot properly and load and execute your code, and you're off to a good start towards getting a more traditional “Hello, world!” program running. If the square wave isn't there, you'll likely want to drop into the assembly-language boot code, perhaps using the output pin to reveal just where your code is failing.

In fact, an output bit is a great way to track down any power-on lock-ups in an embedded system, especially the kind that mysteriously disappears when the debugger is running. The simplest way to use the output pin is to call SetHigh() (assuming the default power-on state of the pin is low) at some point in the section of code you suspect is the problem. Compile, load, and reset the system. If the output is high when it crashes, you can be pretty confident it passed the point in your code where you called SetHigh() . Move the SetHigh() call a little further along, compile, load, reset, and look at the pin again. Eventually, you'll figure out where it failed-though this is a slow, tedious process.

One way to reduce the number of compile-load-reset cycles is to pulse the output pin high then low, instead of setting it high and leaving it there. You can do this at several points in your code, and capture the pulses on an oscilloscope. By counting the number of pulses, you can quickly determine how far your code got.

Sometimes code boots properly but locks up-often quite unpredictably-while running. A digital output can help us determine whether the code is stuck in the middle of a suspect routine when this happens. Modify the routine to set the output high on entry, then low on exit (and don't use that output anywhere else!), something like this:

void SuspectRoutine(void)
{
    SetHigh();
    /*
    * Do some work
    */
    SetLow();
}

Be sure you cover every path through the routine:

int AnotherSuspectRoutine(int arg)
{
    SetHigh();
    if (arg < 0)="">
        SetLow();
        return EXIT_FAILURE;
    }

    /*
    * Do some work
    */
    SetLow();
    return EXIT_SUCCESS;
}

If there is any way the routine can exit without setting the output back to low, you won't be sure the routine completed successfully.

Now the pin will pulse high whenever your routine is being executed. If the output bit is high after the code locks up, the code is stuck in that routine somewhere.[3] This technique is also useful when you don't have a debugger, or the debugger doesn't deal well with interrupt-level code. There's only so much a bit can do, though-it's up to you to track down the source of the problem.

The same modification to the routine can be used to measure how long it takes to execute the code. Don't rely on a single acquisition to characterize a routine. The timing can vary depending on arguments, interactions with other tasks, waiting for shared resources, interrupts, and other factors.

To get a picture of how the routine behaves, you'll need to capture a large number of pulses, each representing a single pass through the section of code you've instrumented. This is most easily done with a digital oscilloscope. Once you've set up the scope to trigger reliably, set the display persistence to “infinity,” and let your system run. For the most comprehensive results, fully exercise all parts of the system during the test. After a time, your scope display will look something like Figure 2. In this case, the instrumented code took from 40 to 55 microseconds to execute.

Figure 2: Execution time varies between 40 and 55 microseconds

In many systems, it is at least as important to know how regularly a routine is executed, as it is to know how long it takes to execute. Even with soft real-time constraints, it may be important to know that certain code can be counted on to run with some reliable frequency. For example, if too much time passes between calls to the keyboard scanning subroutine, we might miss keypresses. A single digital output can help measure this as well.

To provide a less confusing scope image, don't leave the pin high during the routine. Pulse it high then low when the routine starts. Set the scope's timebase (horizontal scale) so that two pulses are captured: the one you are triggering on, and the one caused by the next call to the routine. The trigger pulses will always be in the same place, but the placement of the second pulse in each acquisition will vary. In Figure 3, the routine is called every 80 to 115 microseconds. It should be noted that this method will fail to detect missing pulses. For example, it will fail on occasions where the second pulse occurs so long after the triggering pulse that it is lost off the right edge of the scope display. As with any measurements you make, be sure to consider what you may be missing, as well as what you've observed.

Figure 3: This routine is called every 80 to 115 microseconds

Figure 4: The program responds to this event in 4 to 6 microseconds

Another important measurement is response time. How much time elapses between an event and the servicing of the event by the software? Figure 4 demonstrates how this can be measured using our output pin. As with the previous example, the servicing code has been instrumented to generate a pulse on the output pin when it is called. We also need a signal that indicates when the stimulus occurs. This might come directly from the hardware (for example, the stimulus event is a particular pin-such as an interrupt input-changing state), or might be generated on a second output pin by software (say our stimulus event sends a message to another task; we would pulse this second pin right before sending the message).[4]

In Figure 4, channel 1 is connected to the stimulus pin, and channel 2 to the handler pin. The scope is triggering on channel 1, and the position of the pulse on channel 2 shows the varying response time. In this case, the response time is between 4 and 6 microseconds. Again, it should be noted that this technique will not reveal cases where the response time is longer than the maximum time displayed on the scope screen. It can also display inaccurately short response times, if a second event pulse can occur before the first has been serviced (if the scope should trigger on that second pulse, the handler pulse will be displayed at the incorrect position on the scope screen). If you keep these limitations in mind, though, you can learn a great deal about your system's timing characteristics.

LED control

Sometimes we aren't concerned with timing, and just want to know something about the state of the system. At these times, it can be convenient to have a visual indicator, such as an LED. Our output pin can easily turn an LED on and off, with a little bit of hardware assistance.

Figure 5: Diode

If you've entered the embedded systems world from a software background, a quick introduction to LEDs is in order. Though it looks like a tiny light bulb, an LED-or light emitting diode-is a semiconductor device that only passes current in one direction. Like any diode, an LED has two terminals, known as the anode and the cathode, as shown in Figure 5. When the voltage on the anode is lower than that on the cathode, the diode is said to be reverse-biased. In this state, diodes exhibit extremely high resistance; the LED will be dark. On the other hand, when the voltage on the anode is higher than that on the cathode, the diode is said to be forward-biased, and exhibits very low resistance; the LED will light up.

Because a forward-biased diode has very little resistance, it will look very much like a short circuit if we connect an LED directly across the power supply; a large amount of current will flow through the LED, and it will burn out in an instant. To prevent this, we need to add a resistor in series with the LED to limit the current flowing through it (see Figure 6).

Figure 6: Using a droppingresistor to illuminate an LED

The appropriate value for the resistor depends on the supply voltage, the voltage drop across the LED, and the amount of current we want to allow. Diodes are unusual devices in that, when they are forward-biased, the voltage drop across the diode will be fairly constant, even as the current flowing through it varies. The voltage drop across an LED is typically about 1.7V. If we are using a 5V power supply, that leaves a voltage drop of 5V – 1.7V = 3.3V across our resistor. To obtain a current of 10mA through our LED, then, we can apply Ohm's law: R = 3.3V/10mA = 330W. If we connect our LED, forward-biased, in series with a 330W resistor across our power supply, the LED will safely light. So, if we connect the LED to the output pin, instead of directly to +5V, the LED will light when we set the output bit high, right?

Unfortunately, this rarely works. Most processor pins cannot provide enough output current, so, despite our careful calculations, the LED will remain dark. There are exceptions, such as Microchip's PIC processors, which can source up to 20mA, and can thus drive an LED directly. For most processors, though, we'll need to use a transistor as a switch: the output pin on the processor will turn the transistor on and off, and the transistor will then turn the LED on and off.

Figure 7: Bipolar transistors

Figure 8: Setting the output high will light the LED (NPN)

Many different types of transistors exist, but for this application we'll use bipolar transistors. Bipolar transistors come in two varieties: NPN and PNP, represented by the symbols shown in Figure 7. These transistors have three terminals: base, emitter, and collector. When the base of an NPN transistor is at a higher voltage than the emitter, a small amount of current flowing from the base to the emitter turns on the transistor, and allows a much larger current to flow through the collector. Figure 8 demonstrates how this can be used to let our output pin control an LED. The output is connected to the base of an NPN transistor through a 1,000W resistor. This resistor limits the amount of current the circuit can draw from the processor through the output pin, and protects the transistor as well. The LED is connected to the 5V DC power supply through a current-limiting resistor, and to the collector of the transistor. The emitter of the transistor is connected to ground. When the output pin is low, the base and emitter are at the same potential, and the transistor is turned off. Since no current can flow through the collector, the LED is dark.

When the output is set high, the base is now at a higher voltage than the emitter, and the transistor turns on. Current can now flow through the resistor and LED, through the collector and the emitter to ground; the LED will be lit. A voltage drop of about 0.6V will occur across the transistor, so the current-limiting resistor has been changed to 270W. (5V minus 1.7V across the LED and 0.6V across the transistor leaves 2.7V across the resistor; for 10mA to flow through it, the resistance must be 270W).

Figure 9: Using a PNP transistor, the LED will light when the ouput is low

A PNP transistor behaves much like an NPN transistor, with the polarities reversed. The transistor will be turned on when the base is at a lower voltage than the emitter. In Figure 9, the output from the processor is once again connected to the base of the transistor through a resistor, but this time the collector is grounded, and the LED is connected to the emitter. Now, when the output is low, the base is lower than the emitter and the transistor turns on, lighting the LED. Similarly, when the output is high, the transistor turns off and the LED goes dark.

Note that the LED and resistor can just as easily be connected to the emitter, instead of the collector, of the transistor, as shown in Figures 10a and 10b. You can even put the resistor and LED on opposite sides of the transistor (that is, connect the LED to the collector, and the resistor to the emitter). When you're adding the LED yourself you can use any of these configurations, but you should be able to recognize them all, in case you encounter them in someone else's design.

Figure 10a: An alternate arrangement for controlling an LED with an NPN transistor

Figure 10b: An alternate arrangement for controlling an LED with aPNP transistor

Bipolar transistors are wonderful for turning on LEDs, but are not suitable for handling large amounts of current. If you're working with systems that need to control large currents, you may see a bipolar transistor used to control a relay, which will in turn control the larger current needed by the system. A circuit for this would look much like the LED circuits we just discussed, but with a relay instead of an LED and resistor. Or the circuit might use a Field Effect Transistor (FET) instead of bipolar transistor to control the current. In that case, the circuit would look like the ones we've already seen, but with the LED and resistor replaced by the high-current load, and the bipolar transistor replaced with an FET. The details of constructing such a circuit are beyond the scope of this article, but if you encounter them in a system you are programming, you should recognize them, and be able to control them with confidence.

Inputs

So far, we've focused almost exclusively on output, but we can also use processor pins for input. The most straightforward input to read with a single bit is the state of a switch. One way to do this is to connect the switch between the input pin and the positive supply voltage. When the switch is closed, the pin is connected to the positive voltage, and the processor will read a “1” for the corresponding bit.

Figure 11: Input switches with pull-down and pull-up resistors

When the switch is open, however, the pin is not connected to anything-it's “floating.” Since that condition could produce unpredictable results we avoid it by connecting the pin to ground through a “pull-down” resistor, as shown in Figure 11. This resistor will ensure that the processor will read the pin as low (that is, 0) when the switch is open. When the switch is closed, the pin will be high, and a large value for the resistor ensures that it will not waste much current in this state.

As Figure 11 shows, the switch can be connected between the pin and ground, so that the processor will read the pin as low when the switch is closed, and as high when the switch is open (just the reverse of the previous example). In this case, a “pull-up” resistor is added between the pin and the positive supply voltage, to ensure that the pin is high when the switch is open.

While a pull-up or pull-down resistor will ensure the pin goes to the proper state when the switch is opened, it is important to realize the the voltage will not immediately jump to the correct value when the switch is opened or closed, but will bounce back and forth between 0 and 1. While the value read from the input pin will stabilize immediately in human time, your code can easily read the wrong value. To be confident you know where the switch is set, you should “debounce” the input, by reading it repeatedly, with a short delay between reads, until you get the same value twice in a row. The appropriate delay depends on the characteristics of the circuit, but in general is not critical. If you are polling the input at a relatively slow rate, you can wait until you read the same value on successive polls before accepting the result. If you need to detect switch changes faster, an oscilloscope can show how long the voltage takes to settle, to help you select a delay.

The switch can be a push button, which your program can monitor, and respond to any way you like. For instance, you could dump information about the state of your system to the serial port whenever the button is pressed. Or you can connect one or more input pins to DIP switches or jumpers on your board. Your code could read those pins at power-up, and adjust run-time parameters based on their values. This is frequently done to allow the end-user to configure the system; you can easily configure your own private parameters as well, such as generating verbose debugging information or enabling thorough internal error checking when a particular jumper is in place.

An input pin can also be tied to other devices that can generate logic-level voltages (0 or 1). For example, a simple comparator could be used to tell the processor whether the system's batteries have dropped below a certain voltage level, or if the temperature of a sensitive device is too high. Such applications are beyond the scope of this article, but resourceful programmers should be able to find or develop the necessary expertise when they need to read this sort of input.

One bit at a time

You can accomplish a lot with a single I/O pin. You've learned to write code to read or write such pins, and seen examples of circuitry for basic I/O applications. You should be able to recognize such circuits when you encounter them in a system you've been asked to program. In a pinch, you can now rig up your own hardware for reading an input switch or lighting an LED. I hope this knowledge comes in handy, and helps you create ever better embedded systems.

Mike Gauland received his formal education from SUNY Buffalo and Stanford University, but honed his embedded skills at the benches of Tektronix. He has been developing embedded software for more than a dozen years, and is currently a senior research engineer at Corning He welcomes feedback at .

Endnotes

1. Another solution available on some processors is to drop into assembly language to invoke a processor-specific command to set or clear a single bit in a register. For example, *TCR |= 0x80 could be replaced with something like __asm(“BSET#7,TCR”).
back

2. If you're not comfortable using a 'scope, consult Tektronix's “XYZs of Oscilloscopes” application note (www.tektronix.com/Measurement/App_Notes/XYZs/).
back

3. Or your system has wandered off on its own, perhaps executing data instead of code, and just happened to set or clear the output pin by chance. If you can get the system to consistently lock up, you can try to rule out this possibility by seeing if the pin's state matches whether you call SetHigh() in the routine or not.
back

4. In the latter case, we could get by with a single pin, but having the stimulus code set the bit high, and having the handler set it low. Then, the width of the resulting pulse would reveal the response time.
back

Return to December 2001 Table of Contents

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.