Pages

Wednesday, October 12, 2011

Chapter 8: The AVR I/O

What you have read in my blog so far were for preparing you to be able to program a Microcontroller. Now you can write some code in AVR Studio and program the AVR flash memory with the compiled hex file. Its time to go mainstream. From this chapter lets start building our dreams. You will see what a simple block of code can do. Its time to experience it. Its amazing, its fun and it will evaluate the creative part of your mind.

I will go with coding techniques and algorithms for various functions of a microcontroller. Most of them are interrelated. I will explore them in a certain sequence. This chapter deals with I/O; the most essential portion of an embedded system.


Concepts of I/O
I/O is the shorter version of the term Input/Output. In every programmable device, there is some sort of interface to communicate with the outside world.

Ports & Pins
The total number of pins varies from device to device. For example, ATmega8 has 28 pins, ATmega16 has 40 pins and the smallest one ATTiny15 has 8 pins only. We can't use all the pins for our I/O purposes. Some pins are reserved for some special jobs; for example VCC, GND etc. Different pins of a MCU perform different functions. For our I/O needs, some pins are available in a collective form called PORTS. A port means a collection of Pins. Generally, in an 8 bit MCU, each port consists of 8 pins.
Fig: Cable and Port Analogy

Here is an analogy for you between a cable consisting of several lines and a port that consists of several pins.


The pins in a port are combined for convenience. All the 8 pins in a port can be accessed using a single line of code. When we will need only one or two I/O pins, we will control and access them individually; leaving the remaining pins available for other tasks.

For ATmega8, lets look at the following diagram,

Fig: Pin Diagram of ATmega8

Can you identify the Ports and their corresponding pins in this diagram? ATmega8 has 3 bidirectional ports. The term bidirectional tells us that the pins can act as either input or output. The I/O ports of atmega8 are,
  • PORTB: From PB0 to PB7, total 8 pins
  • PORTC: From PC0 to PC6, total 7 pins (its just an exception)
  • PORTD: From PD0 to PD7, total 8 pins
So we have a total of 23 bidirectional lines in ATmega8. The total number of available ports and pins is an important design issue for an embedded system. You should choose the model that provides sufficient ports for your job.

Elements of the I/O ports
I have said a little about registers in Chapter 1. These are temporary memory locations that can be used to store data or different settings of the MCU. Each AVR port has three separate registers related to it,
  • A port register, for example PORTB.

Bit
7
6
5
4
3
2
1
0
PORTB
PB7
PB6
PB5
PB4
PB3
PB2
PB1
PB0
Initial Value
0
0
0
0
0
0
0
0
Read/Write
Both
Both
Both
Both
Both
Both
Both
Both
  • A data direction register, for example DDRB.

Bit
7
6
5
4
3
2
1
0
DDRB
DDB7
DDB6
DDB5
DDB4
DDB3
DDB2
DDB1
DDB0
Initial Value
0
0
0
0
0
0
0
0
Read/Write
Both
Both
Both
Both
Both
Both
Both
Both
  •  A PINx register (Read Only), for example PINB.

Bit
7
6
5
4
3
2
1
0
PINB
PINB7
PINB6
PINB5
PINB4
PINB3
PINB2
PINB1
PINB0
Initial Value
N/A
N/A
N/A
N/A
N/A
N/A
N/A
N/A
Read/Write
Read only
Read only
Read only
Read only
Read only
Read only
Read only
Read only

All of these consist of 8 bits. PORTB contains the value of the 1 Byte data we assign in our code as output. The value of DDRB determines  the direction of data flow in port B. The PINB register provides the value of the input we have given in the physical pins.

Controlling the Direction
There is a dedicated control register for each AVR port named Data Direction Register(DDRx). Here x is the port name. For the three ports in ATmega8, there are three DDRx registers,
  • PORTB: DDRB
  • PORTC: DDRC
  • PORTD: DDRD
As the name implies, these registers are used to specify the direction of data flow in the MCU. These are 8 bit registers that can be programmed in our code.

The DDRx registers are used to control the direction of the ports. We can control the pins individually or the port as a whole. The DDRx configuration affect the ports as given below,
  • The value '0' in a DDRx bit will set the corresponding bit of PORTx as input
  • The value '1' in a DDRx bit will set the corresponding bit of PORTx as output.
Lets see some code examples,

DDRB=0b11111111; //Set all the pins of PORT B as output
DDRD=0b11100110; //Set PD0, PD3, PD4 as input, the rest of PORT D as output
DDRC=0b00000000; //Set all the pins of PORT C as input

The codes are explained in the comments written in green. The above values are assigned in the binary format. They can be assigned in the hexadecimal format too. The hex version of the above code is written below,

DDRB=0xFF; //Set all the pins of PORT B as output
DDRD=0xE6; //Set PD0, PD3, PD4 as input and the rest of PORT D as output
DDRC=0x00; //Set all the pins of PORT C as input


Accessing the ports
When some pins are set as output, they will give logic level HIGH(1) or LOW(0) according to their values stored in the corresponding bits of the PORTx register. When some pins are set as input, they will be in the high impedance mode. We have to read the pins using the corresponding PINx registers. All the PORTS are initially set as input by default.

Here are some examples showing how to assign values as output and read the values as input.

Examples of output

DDRB=0xFF;         //Set all the pins of PORT B as output

PORTB=0xFF;        //Make all the pins of port b high

PORTB=0b10100101;  //Make PB0, PB2, PB5, PB7 high and the rest of the pins low.

PORTB=0xA5;        //Same as previous line. Equivalent code in hex.




Examples of input

DDRB=0x00;       //Set all the pins of PORT B as input

unsigned int i;  //Declare a variable

i=PINB;          //Read the 8 bit value at port B and save it in the variable i



Examples of Mixed I/O

DDRD=0b11100110; //Set PD0, PD3, PD4 as input and the rest of PORT D as output
 


To access the individual pins, we need a different coding style that will be discussed a bit later.



Tips and Tricks: How to use a LED
A LED is a Light Emitting Diode that can be used in your circuit to indicate the logic level. As you might want to know what is the output level at a certain Microcontroller pin, the easiest way is to cannect a LED at that pin. The logic high level(1) means 5V and logic low level(0) means 0V

A diode is a device that permits the flow of electricity strictly in one direction. It has got some polarity. The current can always flow from anode to cathode but never in the opposite direction. There are two different biasing that can occur to a diode,

  • Anode Voltage > Cathode Voltage : Forward Bias
  • Cathode Voltage > Anode Voltage : Reverse Bias
The important thing is that when a diode is connected in reverse bias, no current flows and when it is connected in forward bias, ideally it acts as a short circuit and huge current can flow through it.

Fig: A diode connected in forward bias

Fig: A diode connected in reverse bias

However, for a LED, a voltage of around 1.4V is dropped across it. If we want to connect it to a 5V source, we need to drop the remaining 3.6V somewhere else; otherwise a huge current will flow through it damaging it completely. We should connect a resistor in series with the LED to limit the current. The LED must be connected in a forward bias.

Fig: A LED with a current limiter

Generally, try to use 1k resistors for this purpose. You can use resistors of any range from 220ohm to 1k. It should be noted that if a LED is operated at a higher current than the nominal range, it can shorten the LED lifetime. The nominal current for many LEDs is around 2mA.

How to identify LED polarity:
  • The two pins of the LED are unequal. The longer one is the anode and the shorter one is the cathode. 
  • Using multimeter: Connect the multimeter probes with the LED. If you connect it in forward bias, a forward voltage will be shown which may be around 1.4 volts or so. If you connect in reverse bias, you will not see any voltage.


Practical Example: LED Flasher
We want to write a program where we will flash some LEDs. The LEDs will be On and Off periodically. The code block should be like this.
----------------------------------------------------------------------------------------------
    // Task 1: Include the required header files


void main()
{

    // Task 2: Initialize the Ports


    for(;;)
    {    

    // Task 3: Turn the LEDs On
    // Task 4: Spend some time
    // Task 5: Turn the LEDs Off
    // Task 6: Spend some time
    }

}
----------------------------------------------------------------------------------------------

Lets do it one by one. We include "io.h" from the "avr/" directory as the header file in Task 1.

#include <avr/io.h>

We will use the lower 4 bits of PORTC as output. So we do not want to waste all the pins in this code. For Task 2 write,

DDRC=0x0F;

The code will now look like this,

----------------------------------------------------------------------------------------------
#include <avr/io.h>


void main()
{

    DDRC=0x0F;


    for(;;)
    {    
    // Task 3: Turn the LEDs On
    // Task 4: Spend some time
    // Task 5: Turn the LEDs Off
    // Task 6: Spend some time
    }

}
---------------------------------------------------------------------------------------------- 

Now lets go inside the loop. The for loop will run endlessly and our LEDs will flash in a periodic rhythm. We have to just define a single period in this loop. To turn on the LEDs, we need to write 1 in each of the lower 4 bits of PORTC register.

There is a very important point here. We want to make the lower 4 bits of PORTC high. You may think of assigning like this,

PORTC=0x0F;

This code will make the lower 4 bits high but at the same time will assign 0 values to the higher 4 bits! This is not expected. These pins may be used for other purposes, we can't change them like this!!

The solution to the problem is,

PORTC = PORTC | 0x0F;

This is an OR operation. The code will make the lower nibble of PORTC high and will not affect the other undesired bits. This code can be written in short like this,

PORTC |= 0x0F;

We will write this for Task 3.

For Task 5, a similar problem arises. How will you make these pins low keeping the rest unchanged? The solution is given below,

PORTC = PORTC & ~0x0F;

We know that, if we AND any value with a 0, the result will be zero. The ~ operator complements the value 00001111 given here and makes it 11110000. The lower four bits of PORTC will get the zero value when they will be ANDed with zeros. The higher four bits will remain unchanged as the are ANDed with ones '1'. The code is written in more convenient form like this,

PORTC &= ~0x0F;

We will write this for task 5. The code will appear like this,
----------------------------------------------------------------------------------------------
#include <avr/io.h>


void main()
{

    DDRC=0x0F;


    for(;;)
    {    
       PORTC |= 0x0F;
    // Task 4: Spend some time
       PORTC &= ~0x0F;
    // Task 6: Spend some time
    }

}
---------------------------------------------------------------------------------------------- 

Now there only remain tasks 4 and 6 which are same. We need a time delay here. There are many good ways to create accurate time delays but for now we will remain simple. The simplest time delay can be made by creating a loop and wasting some clock cycles in it. Look at the following code,

for(i=0;i<=60000;i++)
{

}

This loop will do nothing but it will spend some time here and cycle for 60000 times. You can assign any value other than 60000. You may think it is a very long delay. But, for a processor running at 1 MHz or more, it may be just a blink of eye!

We have used a variable here named i. You need to declare it at the beginning,

unsigned int i;

The complete code will look like this,
----------------------------------------------------------------------------------------------
#include <avr/io.h>


void main()
{
    
    unsigned int i;


    DDRC=0x0F;

    for(;;)
    {    
       PORTC |= 0x0F;
   
       for(i=0;i<=60000;i++)
       {

       }
       PORTC &= ~0x0F;
      
       for(i=0;i<=60000;i++)
       {

       }

    }

}
----------------------------------------------------------------------------------------------

The following diagram shows how to connect the LEDs to the Microcontroller 



Fig: LED Connection

Notice that, I haven't shown the connections of VCC and GND. Connect them according to the pin configuration. Compile the code in AVR Studio. Program the microcontroller with the hex file. After successfull programming, the LEDs will start flashing.

Tips and Tricks: How to use a Switch
We have seen how to get outputs. Now we want to take inputs from some external devices. The most common input device are the tact switches. In our markets they are known as push switches.
Fig: A Tactile switch



These 4 terminal switches are found in various sizes. The one in the figure is a smaller one. The internal connection diagram is shown below,
Fig: Internal Connections
We don't need all the four terminals to implement the switch. We will need only two terminals. You can use either 1,3 or 2,4 or 1,4 or 2,3. Remember, the terminals 1,2 and 3,4 are shorted.

We have two possible inputs which can be understood by the microcontroller. The are,
  • Logic High, (1) indicated by 5 volts.
  • Logic Low, (0) indicated by 0 volts.
When we give inputs, we must give either high or low. There is no meaning of a unconnected or floating input. A floating input pin of a microcontroller can take any state (high or low) which may be undesired for us.

Lets try to connect a switch that will give high when it is pressed. The following two figures will show you a problem.

Method 1: Incorrect
Fig: The input gets High (5V) as the switch is pressed.
Fig: The input is floating when the switch is released!
We were able to make the input High but the problem is that we were unable to make it Low. To make an input low, we must connect it to the ground. So the circuit must be modified.

Method 2: Incorrect
Fig: Giving a Low (0V) to the input

After connecting it like this we can give Low to the input terminal but we have created a bigger problem. Can you think what will happen if the switch is pressed? The Vcc will be directly shorted to the ground!

Fig: Vcc is short circuited to the Ground


























































Never connect a switch in this manner. The next modification should solve all the problems.
Method 3: Correct
Fig: Switch released, Low input
Fig: Switch pressed, High input
I have connected a pull down resistor here. The resistor value should not be below 1k. Always follow this method while connecting a switch as input. This is a active high switch. (High when pressed) If you want to make a active low switch, just interchange the position of the switch and the resistor.


Practical Example: Input to Output
We would like to write a program which will take input from a particular pin and assign that value as output to another pin. The code structure is,
----------------------------------------------------------------------------------------------
       // Task 1: Include the required header files
 
void main()
{
    
    // Task 2: Declare required variables   
    // Task 3: Initialize the required ports
    for(;;)
    {    
    // Task 4: Read from the input pin
    // Task 5: Assign it to the output pin
    }

}
----------------------------------------------------------------------------------------------
Lets take the input from PB0 and give the output to PC0. We include "io.h" from the "avr/" directory as the header file in Task 1. We will declare a temporary variable in Task 2. We initialize PB0 as input and PC0 as output in Task 3.
----------------------------------------------------------------------------------------------
#include <avr/io.h>

void main()
{
    
    unsigned char i;  

     
    DDRB &= ~(1<<PB0);   //Set only PB0 pin as input
    DDRC |= (1<<PC0);    //Set only PC0 pin as output
    for(;;)
    {    
    // Task 4: Read from the input pin
    // Task 5: Assign it to the output pin
    }

}
----------------------------------------------------------------------------------------------

Notice how we have initialized the ports. This is a compact form. First look at the second line,

DDRC |= (1<<PC0);

The '<<' is the left shift operator. We have left shifted '1' by an amount PC0. PC0 is a system constant which is the value of the bit position. Here PC0 is equal to '0'. The operation is explained below,


DDRC |= (1<<PC0);

which means,

DDRC |= (1<<0);
 
which means,

DDRC |= 0b00000001;

which means,

DDRC = DDRC | 0b00000001;


Which means the first bit of DDRC will be made high and rest of the bits will remain unchanged.

Similarly, the command,


DDRB &= ~(1<<PB0);

will make the first bit of DDRB low keeping the remaining bits unchanged.

Now lets come to Tasks 4. We write a command to read the value from the input,
----------------------------------------------------------------------------------------------
#include <avr/io.h>


void main()
{
    
    unsigned char i;  

     
    DDRB &= ~(1<<PB0);   //Set only PB0 pin as input
    DDRC |= (1<<PC0);    //Set only PC0 pin as output
    for(;;)
    {    
       i=PINB;               //Read the input from PINB
       // Task 5: Assign it to the output pin
    }

}
----------------------------------------------------------------------------------------------
We have read the 8 bit value from PINB and stored it in a temporary variable i. This is a 8 bit variable which now contains the value of the 8 bits of port c. However, the value we want is only in the first bit position of the variable i. We can not directly assign a single bit from one variable to a single bit of the another variable. For Task 5, I have written the following code,
       
i &= 0b00000001;
if(i==0)
    PORTC &= ~(1<<PC0);
else 
    PORTC |= (1<<PC0);
}

In the first line, I have nullified all the bits of i except our desired one (the first bit). Now, i will contain either 0 or 1 according to the input. I have checked that condition and changed the PC0 pin of PORTC accordingly.

This is the completed code,
----------------------------------------------------------------------------------------------
#include <avr/io.h>


void main()
{
    
    unsigned char i;  

     
    DDRB &= ~(1<<PB0);   //Set only PB0 pin as input
    DDRC |= (1<<PC0);    //Set only PC0 pin as output
    for(;;)
    {    
       i=PINB;               //Read the input from PINB

       i &= 0b00000001;
       if(i==0)
       { 
           PORTC &= ~(1<<PC0);  //Make PC0 low
       } 
       else 
       { 
           PORTC |= (1<<PC0);   //Make PC0 high
       }
    }

}
----------------------------------------------------------------------------------------------
Compile this code and program the AVR with it. Connect the LED to PC0 and the Switch to PB0. The LED will be on when you press the switch and will be off when you release it. This is the circuit diagram. Note, the VCC, GND connections of the AVR are not shown. Remember to connect them to the supply.

Fig: Switch and LED connection diagram.
Exercises
Try the following practice problems to enhance your understanding,
  
  1. Take a input from PB6 and give that value as output at PD3.
  2. Take input from 4 switches connected at PD1, PD2, PC3 and PB7 and give those values as outputs at the pins PD3, PC4, PB0 and PC5 respectively. 4 LEDs will be connected at the output pins. Each LED will be controlled by one Switch independently. For example, the switch at PD2 will control LED at PC4.

1 comment:

  1. This is online project portal. You can purchase your projects online from any where in India. We'll deliver your project at your door step within the time limit. We'll give you whole guidance on the project you ordered. www.embeddedkits.co.in
    B.Tech Final year projects
    B.Tech Final year projects in Greater Noida | Final Year Project Guidance in Greater Noida | Final Year Project Training in Greater

    ReplyDelete