Because AVR
microcontrollers are Harvard architecture they have separate address
spaces for different memory types: SRAM, FLASH and EEPROM. Each of
these memories have their own purposes. If data will not change during
all time of program execution, then it should be stored as one type of
memory like EEPROM otherwise if data will be used frequently and will
change during program execution, then it should be stored in SRAM
memory. Where to store data decides program developer. One thing you
should note, that working with different types of memories require
different operations how they are accessed.
Usually when define variables in C language like int a – compiler automatically stores it in to the SRAM. But if you want you can place constants in EEPROM or even FLASH memories.
As
I mentioned no specific operations aren't needed to work with variables
sored in SRAM. Lets go through other two memory types FLASH and EEPROM.
Lets say we decide to store some information to FLASH in order to save space in SRAM. Then we
need to show compiler to store this information to FLASH memory. For this wee will need additionally library: #include <avr/pgmspace.h>. Now wee can declare data to be stored in FLASH memory:
// custom LCD characters
const uint8_t backslash[] PROGMEM=
{
0b00000000, //back slash
0b00010000,
0b00001000,
0b00000100,
0b00000010,
0b00000001,
0b00000000,
0b00000000
};
Declaring variable with keyword PROGMEM shows compiler to store this constant in AVR FLASH memory.
For reading from FLASH usually use pgm_read_byte() function like in this example:
uint8_t a, i;
for(i=0;pgm_read_byte(&backslash[i]);i++)
{
a=pgm_read_byte(&backslash[i]);
}
this way you can store and read bytes, words and double words by using commands:
pgm_read_byte()
pgm_read_word()
pgm_read_dword()
If you will read pgmspace library documentation you will see that instead const uint8_t backslash[] PROGMEM you can use const prog_uint8_t backslash[]
Here is a working example of program:
#include <avr\io.h>
#include <avr\iom8.h>
#include <avr\pgmspace.h>
const prog_uint8_t backslash[]=
{
0b00000000,//back slash
0b00010000,
0b00001000,
0b00000100,
0b00000010,
0b00000001,
0b00000000,
0b00000000
};
int main(void) {
uint8_t a, i;
for(i=0;pgm_read_byte(&backslash[i]);i++)
{
a=pgm_read_byte(&backslash[i]);
}
while(1) {
}
}
Char types
As it is important to understand variable types in AVR-GCC lest take a deeper look at this.
Choosing
proper variable type sometimes may be crucial in designing embedded
software as microcontroller resources are limited. Choice of proper
variable type may save lots of time and space of your program.
What
is common variable type in 8 bit system? Of course byte or so called
char type. Char name comes from word “character” because char type
usually is used to store character symbols (ASCII). Char type can have
values from -128 to +127 while unsigned char may have values from 0 to
256. Some compilers may use byte type instead of unsigned char.
User types
C language allows defining user types. For this typedef keyword is used:
typedef unsigned char byte; //create byte type
typedef unsigned int word; //create word type
In other words defining custom types description structure is used:
typedef standard_type custom_type;
WinAVR compiler has predefined custom types:
typedef signed char int8_t; //located in header file inttypes.h
typedef unsigned char uint8_t; //located in header file inttypes.h
typedef int int16_t; //located in header file inttypes.h
typedef unsigned int uint16_t; //located in header file inttypes.h
typedef long int32_t; //located in header file inttypes.h
typedef unsigned long uint32_t; //located in header file inttypes.h
typedef long long int64_t; //located in header file inttypes.h
typedef unsigned long long uint64_t; //located in header file inttypes.h
typedef struct {int quot; int rem} div_t; //located in header file stdlib.h. It is used for standard function ldiv();
All AVR ports have
Read-modify-write functionality when used as genera I/O ports.
Direction of separate port pin can be changed. Each pin buffer has
symmetric capability to drive and sink source. Pin driver is strong
enough to drive LED directly , but it is not recommended. All port pins
have selectable pull-up resistors. All pins have protection diodes to
both VCC and GND.
Each
port consists of three registers DDRx, PORTx and PINx (where x means
port letter). DDRx register selects direction of port pins. If logic
one is written to DDRx then port is configured to be as output. Zero
means that port is configured as input. If DDRx is written zero and
PORTx is written logic “1” then port is configured as input with
internal pull-up resistor. Otherwise if PORTx is written to zero, then
port is configured as input but pins are set to tri-state and you might
need to connect external pull-up resistors.
If PORTx is written to logic “1” and DDRx is set to “1”, then port pin is driven high. And if PORTx=0, then port is driven low.
Lets
move to C. How we can control AVR port pins? When using WinAVR GCC
first we need to set up proper library where PORT register names have
their addresses defined. Main library where general purpose registers
are defined is io.h located in avr directory of WINAVR installation
location.
#include <avr/io.h>
Now
we can use port names instead of their addresses. For instance if we
want to set all pins of PORTD as output we simply write:
DDRD=0xFF; //set port D pins as outputs
Now we can output a number to port D:
PORTD=0x0F; //port pins will be set to 00001111
if we have 8 bit variable i we can assign this variable to port register like this:
uint8_t i=0x54;
PORTD=i;
Lets read port D pins:
DDRD=0; //set all port D pins as input
i=PIND; //read all 8 pin bits of port Dand store to variable i
There is ability to access separate port pins. So all eight port pins can be used for multiple purposes.
Some of the pins may be configured as outputs and some as inputs and performs different functions.
Lets say we need 0,2,4,6 pins to be as input and 1,3,5,7 as output. Then we do like this:
DDRD=0; //reset all bits to zero
DDRD
|=(1<<1)|(1<<3)|(1<<5)|(1<<7); //using bit
shift “<<”operation and logical OR to set bits 1,3,5,7 to “1”
So we can output values to 1,3,5 and 7 pins
PORTD |=(1<<1)|(1<<3)|(1<<5)|(1<<7);
Or clear them
PORTD &=~((1<<1)|(1<<3)|(1<<5)|(1<<7));
Reading of port pins is easy. Set any pin(s) for input like this:
DDRD &=~((1<<1)|(1<<3)); //This clears bits 1 and 3 of port direction register
i=PIND; //reads all 8 pins of port D
You
can read 1 and 3 bits by using masks or simply shift i value by 1 ar 3
positions to compare LSB with 1. Of course there are some functions in
sfr_defs.h library like bit_is_set() or bit_is_clear() to check particular bits and make these tasks little easier.
Following example should clarify some issues:
#include "avr\io.h"
#include "avr\iom8.h"
int main(void) {
DDRD&=~_BV(0);//set PORTD pin0 to zero as input
PORTD|=_BV(0);//Enable pull up
PORTD|=_BV(1);//led OFF
DDRD|=_BV(1);//set PORTD pin1 to one as output
while(1) {
if (bit_is_clear(PIND, 0))//if button is pressed
{
PORTD&=~_BV(1);//led ON
loop_until_bit_is_set(PIND, 0);//LED ON while Button is pressd
PORTD|=_BV(1);//led OFF
}
}
}
Microcontrollers without interrupts are almost
worthless. Interrupt is called what interrupts normal program flow.
Interrupts are nothing more than subroutines that put on hold normal
program flow while they are executed. After interrupt subroutines are
finished they return to normal from last point. Those subroutines are
called interrupt handlers that are called by some event like toggle
external pin or some register value overfilled like timer overflow and
so on.
Why interrupts are so important? For instance without
interrupts you would have to do loops in order to check if one or
another event occurred. This operation is called polling. But pooling
has many disadvantages like program have do loops taking resources from
other task may be done. This is why microcontroller comes with multiple
interrupt sources. Instead of checking events microcontroller has
ability to interrupt normal program flow on event and jump to interrupt
service subroutine and then get back to normal program flow.
What
happens when interrupt occurs? After interrupt event microcontroller
stops the task at current point, saves current information to stack and
gives its resources to interrupt subroutine. When interrupt subroutine
is finished last program point is restored and program flow continues.
In assembly language your program usually would start with interrupt table:
This
table indicates where all interrupt subroutines are located. After
particular interrupt occurs, program pointer jumps to interrupt table
where it is directed to interrupt subroutine location.
For
instance every time you power up microcontroller program pointer jumps
to location $000 where it is directed to (RESET) location. With C
program this would be location of main() function.
C
Compiler creates this table while compiling the source code. But how to
describe interrupts handling routines in C language using WinAVR
toolset. All time when compilers are improving, there is no agreement
how to handle interrupt code. As compilers tries to stay away from
machine dependant details, each compiler is designed with their own
methods. AVR-GCC
isn’t exception. There each interrupt is pointed with predefined names
– you will find in microcontroller definition header file like in
iom8.h.
Defining the interrupt routines is ease…
First of all include desired library that compiler could understand interrupt macro commands:
#include <avr/interrupt.h>
All interrupts then can be described using macro command: ISR(), for instance:
ISR(ADC_vect)
{
//Your code here
}
This
subroutine is for ADC conversion complete handler. Macro command ISR()
is convenient for handling all unexpected interrupts. Just create
routine like this;
ISR(_vector_default)
{
//your code here;
}
If you want to describe an empty interrupt for particular event (puts “reti” command in interrupt table) just use:
EMPTY_INTERRUPT(ADC_vect)
Example:
Simple program:
#include <avr\io.h>
#include <avr\iom8.h>
#include <avr\interrupt.h>
#define outp(a, b) b = a
#define inp(a) a
uint8_t led;
typedef unsigned char u08;
ISR(INT0_vect) { /* signal handler for external interrupt int0 */
led = 0x01;
}
ISR(INT1_vect) { /* signal handler for external interrupt int1 */
led = 0x00;
}
int main(void) {
outp(0x01, DDRB); /* use PortB for output (LED) */
outp(0x00, DDRD); /* use PortD for input (switches) */
outp((1<<INT0)|(1<<INT1), GIMSK); // enable external int0, int1
sei(); /* enable interrupts */
led = 0x01;
for (;;) {
outp(led, PORTB);
} /* loop forever */
}
In earlier versions of WINAVR there were SIGNAL() macro used. Now this is the same as ISR() and not used anymore.
Another
interesting interrupt handling macro is ISR_ALIAS(vector,
target_vector). This is used when you need to pint one vector o another
vector. This way you may handle multiple interrupts with single
implementation, for instance:
ISR(INT0_vect) { PORTB = 42; } ISR_ALIAS(INT1_vect, INT0_vect);
Ref : http://winavr.scienceprog.com