The basic and most common function of a uC is to provided digital input and output. Working with the inputs and outputs of the uC is a fairly simple game of playing around with a few bits and bytes. Let the games begin.
Registers are the work space of the processor. Registries are one byte (8-bits) in size. Some can be read from, written to, or both depending on the particular function. There are many different types of registers in the uC. Some registers give you access to information on the current state of the processor. Other registers allow you to control the configuration of the processor's internal peripherals. Think of them as switches that allow you to turn things on and off. Each bit in the register can represent a setting of particular parameter like the digital input and output of the uC.
The digital I/O of the uC is controlled by three registers DDRx, PINx, and PORTx. With AVRs every eight I/O pins are grouped into a port and are designated a letter. In the case of the Tiny13 it's only port is labeled "B". The "x" in the register's name is replace with the port's designation so for the Tiny13 it would be DDRB, PINB, and PORTB.
Each bit in the digital I/O registries represents an digital I/O pin on the IC. The digital name of each pin carries the port name and bit number of the pin. In the case of PB0 it is on port B and is represented by bit zero. PD7 is on port D and is represented by bit seven.
Note: The bit number does not always correlate to the physical pin number of the package. You will have to look up the pin name and position in the datasheet.
DDRx: Data Direction Registry. Most I/O pins of the uC can be configured as either an input or an output. This registry controls which direction an I/O pin will be. Low will set the pin as an input. High will set the pin as an output.
PINx: Port Input registry. If the pin has been configured as an input the current input state will be stored in this registry.
PORTx: Port data registry. If the pin has been configured as an output the state will be controlled from this registry.
If a pin is configured as an input in DDRx setting the pin's corresponding PORTx value high enables an internal pull up resistor.
Tip: Some newer uC allow you to write to PINx while the pin is configured as an output. The result will be a toggle of the pin's current value.
Note: When using various internal peripherals you may need to configure the I/O pin(s) to a specific state. Read the datasheet section on the particular function for information on how to configure the pin.
When a pin is configured as an output it will sink or source current. When a pin is configured as an input the pin goes into a high impedance state (a.k.a Tri-State or Hi-Z) so it should not cause any loading issues on the input signal.
There are a few other handy features that various combinations of these registries will produce. For example if we were using a pin as an input controlled with a switch. In order to get high and low state you would need a double throw switch to switch between height and low or a single throw switch connected to a power line with a pull up/down resistor. In either case you have to make an extra connection. The AVR uCs have an internal pull up resistor on each of it's I/O pins that can be enabled as described above. Once enabled all you need for a switch input is a single pole switch to ground. This makes the design smaller, less complex, and cheaper. Of course enabling the internal pull up will change the high impedance characteristic of the pin.
Most uC have a pin for hardware resetting of the uC just in case. On the Tiny13 it is the physical pin 1 labeled PB5 so this means the pin must be set as an input. The resulting input would be left floating allowing the possibility of spontaneous resetting under certain conditions. To prevent this you can enable the internal pull up on the pin.
On some uC, like the Tiny13, there is a way to use the reset pin as a true input/output pin. You can disconnect the reset function internally but the gaining of a general purpose I/O pin comes a price. For one you cannot externally reset the uC. Another problem is the uC is programmed through ISP which uses the reset pin for the programming sequence. This means disabling the reset pin disables further ISP programming. However you are not stuck with a un-programmable uC if you have a programmer capable of high voltage serial programming like the STK500. You can look up the instructions on how to use the high voltage programming ability of the STK500 to program the processor or re-enable the reset pin.
Most logic operators have two inputs. When the term "explicit value" is used this will refer to a fixed value being given at one input and will not change. The other input being evaluated will be called the "initial value" and is the variable value.
The terms low, off, 0, clear, and zero will be used for a logic level of "0".
The terms high, on, 1, set, and one will be used for a logic level of "1".
Bit values will be of one bit in a whole byte although other bits of the byte may be omitted.
Decimal values will be displayed as normal. Heximal values will be represented with a "0x" in front of the value. Binary values will be represented as a 8-bit byte with all eight bits displayed.
Heximal will be used for constant byte values. Note: This is standard practice.
Bits in a byte are represented from Least Significant Bit (LSB) to Most Significant Bit (MSB), meaning right to left when you look at the byte, as 0 to 7 not 1 to 8.
As explained earlier controlling the digital I/O of the uC is a matter of manipulating the registries. The I/O registries are mapped into the address space so we can work with these registers from C like normal 8-bit variables. This makes working with them very easy for us, if you know what you are doing.
If we know what the value of the all the bits are going to be we can assign the value directly to the register setting all the bits in one step. Simple enough but the problem arises when we want to work with just one I/O pin thus one bit in the register. You could try to calculate the value of a new byte from the old byte with some fancy math but the much easier way is to use operators.
As a C programmer up until now you probably have only written software for larger platforms like the desktop. You may have not been taught bit operators or you don't use them often enough so you probably have forgotten them by now. Well their back and this time its personal. Lets go over the bit operators and their truth tables.
|
|
|
|
Note: Do not confuse bit operators like "&" and "|" with logical operators like "&&" and "||".
Also you need to know the shift operators which are "<<" and ">>". ">>" Shifts bits to the right and "<<" shifts bits to the left. They are used as "x >> y" and "x << y" where x is the operand and y is the number of shift operations.
Now that we have had our little operator review lets talk about how we use them to manipulate bits to perform functions we need.
In order to successfully manipulate bits you have to not only look at the bit you are changing but the whole byte to make sure you are changing just the one bit you are interested in while preserving the value of the other bits in the byte. We will then have to look at bit manipulation on the single bit level and then the whole byte level to make sure we are doing things correctly.
Tip: If you have trouble converting between decimal, heximal, and binary you can use the windows' calculator in "Expert" mode. There is a row of radial buttons labeled "Dec", "Hex", and "Bin". "Dec" is decimal, "Hex" is heximal, and "Bin" is binary. Select the starting format, enter the value, then click the final format to get your converted value.
For turning a bit on we have to find a logic operation that sets a bit high without regard for the current bit state. If you look at the OR truth table if one input is set to 1 no matter the value of the other input the final output will be 1. The expression for this would be:
value = value | 0x01;
Lets test it out on the bit level.
value initially low:
value = 0x00;
value = 0x00 | 0x01;
or in decimal
value = 0 | 1;
value = 1;
value initially high:
value = 0x01;
value = 0x01 | 0x01;
or in decimal
value = 1 | 1;
value = 1;
Lets see what happens to all the bits in the byte with a initial value that has other bits set to 1. We will do this in binary to make it easier for us to do the operations.
value initially 01010100 (Bit 0 is low):
value = 01010100 | 00000001;
value = 01010101;
value initially 01010101 (Bit 0 is high):
value = 01010101 | 00000001;
value = 01010101;
All the original bits were preserved. Only the bit 0 was changed to 1 regardless of the bit's initial value. We can use this expression to turn a bit on within a byte with no regard to the bit's initial value and without affecting the other bits in the initial value.
Tip: You can shorten the expression by writing the expression as: value |= 0x01;
To turn off a bit we need to use another operation. Looking at the truth tables above AND seems to do what we want if we use an explicit value with bit 0 having a value of 0. Lets try the expression:
value = value & 0x00;
On the single bit level.
value initially low:
value = 0x00;
value = 0x00 & 0x00;
or in decimal
value = 0 & 0;
value = 0;
value initially high:
value = 0x01;
value = 0x01 & 0x00;
or in decimal
value = 1 & 0;
value = 0;
That seems to work. Bit 0 is cleared to 0 no matter the initial value. Lets try it on a whole byte using an initial value with other bits set to 1.
value initially 01010100 (Bit 0 is low):
value = 01010100 & 00000000;
value = 00000000;
value initially 01010101 (Bit 0 is high):
value = 01010101 & 00000000;
value = 00000000;
That didn't work. All the other bit values we cleared to 0. Looking at the AND truth table again tells us that if the explicit value is set to 1 the initial value will be preserved. We need to use an explicit value where each bit in the initial value not being changed is 1 thus preserving the initial value and the bit being changed is 0. We need an explicit value of 11111110. Hey that looks like an inversion of 0x01 (00000001) from a NOT operation. Lets try out the previous AND expression with the explicit value of 0x01 inverted. This would look something like:
value = value & ~(0x01);
Lets run through a full byte using an initial value with various bits set to 1.
value initially 01010100 (Bit 0 is low):
value = 01010100 & ~(00000001);
value = 01010100 & 11111110;
value = 01010100;
value initially 01010101 (Bit 0 is high):
value = 01010101 & ~(00000001);
value = 01010101 & 11111110;
value = 01010100;
Bit 0 was cleared to 0 no matter the initial value and the other bits in the initial value were preserved.
Tip: You can shorten the expression by writing the expression as: value &= ~(0x01);
What else can we do that would be handy. Flipping the value of a bit to it's opposite value would be nice. Looking at the truth tables again leads us to the XOR operator with an explicit value of 0x01. The expression would be:
value = value ^ 0x01;
Lets try using a initial value with various bits set to 1.
value initially 01010100 (Bit 0 is low):
value = 01010100 ^ 00000001;
value = 01010101;
value initially 01010101 (Bit 0 is high):
value = 01010101 ^ 00000001;
value = 01010100;
Bit 0's value was inverted.
Tip: You can shorten the expression by writing the expression as: value ^= 0x01;
Thus far we have been writing to a bit in a byte. Lets look at reading a bit in a byte. Often you will want to read the value of a bit in a byte but you won't care what the other bits in the byte are. You just want a 0 or a 1 returned when checking the value of the bit in a byte.
Looking at some of our previous testing we now know that the AND operation will ignore the value of any bit in a byte if the explicit value bit is 0 and preserve the value of the bit if the explicit value bit is 1. So if we are checking bit 0 we want an initial value of 00000001 or 0x01. The expression would be:
value & 0x01;
Lets try reading bit 0 using a initial value with various bits set to 1.
value initially 01010100 (Bit 0 is low):
01010100 & 00000001 = 00000000 = 0
value initially 01010101 (Bit 0 is high):
01010101 & 00000001 = 00000001 = 1
The value returned is the value of the bit being examined.
We can now turn a bit on, off, flip it's value, and read it's value but up until this point we have only been playing with bit 0 in the byte. We want to be able to perform any of these functions on any bit in the byte. Lets look at how to do that.
We have been working with bit 0 in the byte because we have been using the explicit value of 0x01 (00000001). If we want to work with any of the other bit in a byte would use another explicit value with the bit we are interested in set to 1 and all other bits set to 0. To figure out the explicit value we would need to do some math or if you are a real programmer you would use some operator that is already included like say one of those shift operators that we reviewed earlier.
If we always start with the value of 0x01 (00000001) as the explicit byte by using the shift left operator we can move bit 0 in the byte (1) along to the position we want. The expression would be:
(0x01 << number of steps);
Lets say we want to work with bit 3. We need a byte that looks like this 00001000. So we shift the byte to the left three steps with the expression:(0x01 << 3);
Start: 00000001
Step 1: 00000010
Step 2: 00000100
Step 3: 00001000
Lets combine the shift operation with a expression for example to turn on bit 5:
value = 0x00;
value = value | (0x01 << 5);
value = 00000000 | (0x01 << 5);
value = 00000000 | (00000001 << 5);
value = 00000000 | 00100000;
value = 00100000;
Tip: Notice that the number of shift operations is the same as the bit number in the byte when starting with the explicit value of 0x01. That could make things simpler for future programming.
Now we can turn any bit on, off, toggle, or read it's value.
Here is a table of general bit manipulation expressions summarized:
General Bit Manipulation Expressions | |
---|---|
Turn on bit X in byte Y | Y |= (0x01 << X); |
Turn off bit X in byte Y | Y &= ~(0x01 << X); |
Toggle bit X in byte Y | Y ^= (0x01 << X); |
Read bit X in byte Y | Y & (0x01 << X); |
Thus far we have been modifying the bits in a normal variable. As mentioned earlier the registries DDRx, PORTx, and PINx are mapped into memory so we can modify them as normal 8-bit variables. The annoying thing is that we would have to remember the memory location of each registry and the bit numbers for each pin. Thankfully there is way around this.
The compiler loads a header file through the io.h include for the part specified in the Makefile. The header file contains a set of definitions for the various registries and other information. One set of definitions are the values for the pin names and I/O port registers. This allows us to use the pin and port names instead of remembering the values making for cleaner and clearer looking code. Here are a few different examples.
To turn on PB4:
PORTB |= (0x01 << PB4);
To turn off PB3:
PORTB &= ~(0x01 << PB3);
To toggle PB2:
PORTB ^= (0x01 << PB2);
To read PB0:
PINB & (0x01 << PB0);
Note: Remember to read the pin's input state we have to use the PINB register not PORTB.
That looks easier to read doesn't it. Now to analyze a real program to see what it is doing.
In the introduction article you were given the program "hi_there.c". With our new found skills lets figure out how it works. Lets start by looking at the port assignments.
DDRB = 0x1F; PORTB = 0x20;
At the start of main() DDRB and PORTB are assigned values. DDRB gets 0x1F which in binary is 00011111. That would set all the pins as outputs except PB5 which is the reset pin.
PORTB is assigned 0x20 which in binary is 00100000. This sets all the output pins as low except PB5 which is the reset pin. PB5 is set as an input so setting PB5 in PORTB high turns on the internal pull up resistor on the pin. This will allow the processor to run without the need of a external pull up resistor on the reset pin to keep it stable.
Inside the main() loop there is the expression:
PORTB ^= 0x01 << PB0;
We now recognize that this as a toggle expression. It flips the value of PB0 in PORTB which is an output pin. This is what turns the LED on and off.
The rest of the program is self explanatory. Pretty simple now isn't it.
Now knowing how to work with inputs and outputs we can modify "Hi There!" to incorporate an input making the programming "Hi In There!".
// hi_in_there.c // A simple program that toggles PB0 when a switch on PB1 is pressed #include <avr/io.h> void delay (void); // Delay function prototype int main (void) { DDRB = 0x1D; // Set all pins as output except RESET (PB5) PORTB = 0x22; // Set all output pins low. Enable pull-up on RESET (PB5) while(1) // Main Loop { if ( !(PINB & (0x01 << PB1)) ) // Read PB1 { PORTB ^= 0x01 << PB0; // Toggle pin state //delay(); // Delay loop. Comment this out for faster simulation. } } return 0; } void delay (void) { unsigned int counter = 10000; // Declare counter with intial value of 10000 while (counter--); // Decrement counter until zero }
Here is the downloadable code file:
Being that we are now using a pin as input DDRB has to be changed. In this case we are going to use PB1 as an input so the value of DDRB in binary should be 00011101 which is 0x1D. Since we are going to use a push button on the input we should enable the internal pull up on PB1 so PORTB will be 00100010 which is 0x22;
Note: If you look at the schematic of the STK500's User Switches (Section 3.2 in the STK500 User Guide) you will see there is already an external pull up resistor. This is intended for designs that will have a external pull up resistor so the uC/software can still be tested without having to wire up resistors yourself. It is still safe to enable the internal pull up at the same time as it will not affect the performance of the uC. Even though you could get away without the internal pull up it is a good habit to do it now anyways if you plan on later putting the uC into a circuit with no external pull up so you don't forget.
We can tell the LED to blink if the button is pushed by wrapping the toggle expression in a simple if statement that reads the input pin:
if ( !(PINB & (0x01 << PB1)) ) // Read PB1
Note: Remember that the pull up will cause a 1 to return when the button is not pressed and a 0 when the button is pressed. For that reason we invert the expression the if statement is test for using "!".
Compile your code and start the simulator.
Note: I commented out the delay() loop again to speed up simulation. Don't forget to uncomment the line when you test in hardware.
If you pause the simulator using the "Break" function (Ctrl+F5) you can manually change bit values in the registers under the "I/O View" window by clicking on the bit number/box of the corresponding register. This provides a method to simulate an input like our push button. Click on bit 1 in PINB turn set it to 0 then continue to step through the code. Notice that the toggle section now runs.
Use a jumper to connect PB1 in the PORTB header to SW0 in the SWITCHES header. This connects the SW0 push button, under the LED 0 which we are blinking, to the uC. It will be our push button to start the LED to toggle.
Program the uC as you did before and observe. Try pressing the SW0 button. The LED blinks! Release the button and it stops. We now can control the output from an input.
At this point I think it would be a good idea to leave you and your uC alone for awhile and let nature take it's course. You should experiment with different combinations of inputs and outputs. Try using multiple inputs to control an LED. Try using multiple LEDs with a single input. Try turning on and off an LED or LEDs with no blinking using an input. Try changing the rate of blinking with an input(s). Try the same function but on different pins. Get a good feeling of how to use the registers and bit manipulation expressions.
Note: If you looked at the schematic of the LEDs in the STK500 user guide (Section 3.1) you may notice something alittle funny. The LEDs are configured to turn on when the uC output is low. Remember this when you are playing around.
Your Tone God,
Andrew