Sei sulla pagina 1di 25

Introduction

This tutorial provides an introduction to the capabilities of the DHP Technology


CodeVisionAVR C Compiler and Integrated Development Environment. It does not
pretend to show all the features of this development system – it is just what it says in the
first line; an introduction.

This is not a tutorial for C - we assume that you know it – nor is it a tutorial for
assembly language for the AVR series of microcontrollers. We assume that you are at
least somewhat familiar with the operation of the 90S series of AVR microcontrollers. In
this tutorial, we will just give you some examples of using the CodeVisionAVR C
compiler and all its tools for the easy compilation of programs for the AVR series.

For microcontrollers, the best way to learn is to actually program some devices to
do some simple functions. There are many development boards available; several
companies make them. We have designed this tutorial around Atmel Microcontroller
Starter Kit Development Board (we’ll just abbreviate this to AVRDB) because we think
it is the board you are most likely to have. This small unit and inexpensive unit is readily
available and it may be used to program and run programs for all the Atmel 90S series of
microcontroller chips. The exercises are designed around a 90S2313 microcontroller
plugged into this board but will work equally well with any of its larger brothers – the
90S4414 or the 90S8515. The 90S2313 has 2K bytes (1K instructions) of flash memory,
128 bytes of EEPROM, 128 bytes of RAM and 15 free I/O pins. The other larger
microcontrollers have larger flash memories, greater EEPROM and RAM and more I/O
pins.

It is also assumed that you have installed the ‘demo’ version of the
CodeVisionAVR C compiler in the C:\cvavr directory and that you have installed the
Atmel tools for programming (AvrProg.exe) and debugging (AvrDebug.exe) somewhere
on your computer. It goes without saying that you have to know how to access these
programs and that you are familiar with the Windows 95, Windows 98 or Windows NT
operating system that you are using.
2

Tools

Before starting with some programming, we first need to set up the IDE to make
life easier for ourselves later by configuring several ‘tools’. These tools will allow us to
program the target microcontroller and to debug it using a simulator all from within the
CodeVisionAVR C Compiler window. It is beyond the scope of this tutorial to describe
the detailed use of these tools. We will merely describe how to set them up so that they
can be accessed from within the CodeVisionAVR C Compiler window.

If you have the Atmel AVRDB, you will also have received or can download
through the Web, the two main tools; AvrProg.exe for programming the AVRDB and
AvrDebug.exe for simulating and debugging assembled code. If you have some other
development system, it will have a program for programming the target microcontroller.
We need to tell the CodeVisionAVR C Compiler system where these files are located on
your computer.

You use the Tools | Configure pull-down window to add tools to the
CodeVisionAVR C Compiler window. For example, let’s assume that you have the
AVRDB and want to add the Atmel programming program, AvrProg.exe, to your tool
box. Pulling down the Tools menu and clicking on Configure gives you the following
window. You click on the Add button and a window opens up to allow you to point to
the desired file which you
wish to add to the tool box:
AvrProg.exe. Once you
have selected it, you can
change the settings as you
like. If you intend to go
through the programs
described in this tutorial,
you will want to change the
settings so that the initial
directory which
AvrProg.exe goes to is the
directory containing all the
tutorial programs;
C:\cvavr\examples\Tutorial.

Later, in the course of this tutorial, when you want to actually program a target
microcontroller on the AVRDB, you just pull down the Tools menu and click on the
AvrProg.exe selection to run this program. You may add other programming tools or
remove them at a later time.

2
3

The CodeVisionAVR C Compiler produces files in a format


which is compatible with the Atmel simulator/debugger program
which they call Studio and whose running program is AvrDebug.exe.
You can add this tool to the environment by clicking on the Settings
pull-down menu and selecting Debugger. You will then be prompted
for the location of AvrDebug.exe. Once this has been set, you can run
the Studio program from within the CodeVisionAVR C Compiler
window by clicking on the ‘debug’ button shown to the left.

Finally, the 90S series of microcontrollers were designed to be programmed via


an SPI port. This is called ISP or ‘In System Programming’. The programming is done
while holding the RESET- line of the microcontroller low and sending data to and from
the microcontroller serially. Using ISP means that the target microcontroller mounted on
its target board may be programmed in place via a 10 pin header mounted on the board.
The Kanda STK200/300 series of ISP programmers are simple devices which consist of a
latch mounted on a ‘dongle’ which plugs into the computer’s parallel port. They have a
cable terminating in a ten-pin IDC (insulation displacement connector) which plugs into
the header on the target board. More details on ISP programming are available in Atmel
application notes and at http://www.avr-forum.com/avrsource.html. A circuit schematic
for this dongle is available at
http://members.xoom.com/_XOOM/volkeroth/dongle_e.htm.

The CodeVisionAVR C Compiler system provides ISP using


the STK200/300 dongle. It is accessed through the ‘Chip
Programmer’ button shown to the left. NOTE: you must first add the
isp.exe program to the Tools menu in a way similar to that described
previously to add the Atmel prog.exe program. After doing so and
with the STK200/300 dongle in place, you just need to click on this
button in order to program the microcontroller. Doing so will bring
in a window allowing one to access the microcontroller via the ISP
interface. This window is shown here.
Using the facilities of this window, one
may read the existing program in the
microcontroller, erase it and/or
reprogram it from a file. One may also
read and/or reprogram the contents of
the EEPROM on board the
microcontroller. The flash memory
may be programmed from Intel format
HEX files, from Atmel format .ROM
files or from binary files. The
EEPROM may be programmed from
.EEP files, from HEX files or from
binary files.

3
4

One can also access the signature bytes in the microcontroller and lock the flash memory
to prevent unauthorized access to the program. Once locked, the microcontroller may
only be reprogrammed by completely erasing it first.

4
5

Simple Program

It is customary to write, as a first program, one that puts “Hello world!” on some
output device. Since your first device will be just the AVRDB with a single 90S2313 in
it, this is not really realistic. Instead, we will write a first program in which LED’s are
controlled by push-button switches; all these devices are already on the board.

After starting the


Codevision compiler, you
will see this window.

Using File | Open


commands, ‘open’ the
‘Tutor1.prj’ project in the
‘C:\cvavr\examples\Tutorial’
directory. You will then see
a window with the C code
for this first project. It will
contain the code shown in
the next window. The file
name for this code is
tutor1.c.

5
6

Before going any further, let’s look at this code. The first four lines after the
opening comment are extensions to C which tell the compiler about the ports of the AVR
series. From the viewpoint of the programmer, they assign a variable name to a
microcontroller I/O register. The port D input register, for example, is given the name
‘PIND’ in the first of these four lines and is given the appropriate internal microcontroller
address of (hex) 10.

In the main procedure, we first declare an unsigned character variable which we


name ‘data’. This will be used as our temporary storage when we ‘read’ the switch port.
The next two lines of code show how easy it is to access I/O registers in this compiler.
We want to store the hex byte $00 into the data direction port of port D since we want
port D to be all inputs and to store the hex byte $FF into the data direction register of port
B since it is to be an output port. In assembly language, we would accomplish this by
some code like:

clr r17 ; clear all data bits


out DDRD,r17 ; put this into port D DDR to make these all inputs
ser r17 ; set all data bits
out DDRB,r17 ; put this into port B DDR to make these all outputs

Here, we just state:

DDRD=0x00;
DDRB=0xff;

because we have already defined, with the ‘sfrb’ statements what I/O register address
corresponds to the variable names, DDRB and DDRD.

The next part of the program is a ‘while’ loop which will run forever since the
condition is always true (non-zero). Inside the loop, in the first instruction, we read the
status of the switches:

data = PIND;

The ‘data’ byte will just contain the value at the input to the port D pins. This simple
statement shows how we ‘read’ an I/O register and transfer its value to a variable in the C
code. Easy, isn’t it?

When a push-button switch is pressed, the pin is grounded – an unpressed switch


is pulled high by pull-up resistors on the AVRDB. So, the unsigned char variable, data,
will have bits which are logical ones for unpressed switches and logical zeroes for
pressed switches.

Again, to show how easy it is to access I/O registers in the micro, the next line:

6
7

PORTB = data;

just stores this value into the output port. The output pins are connected to LED’s which
go to the +5V supply through 680 Ohm resistors. When an output pin is low, the
corresponding LED will be lit; when the output pin is high, the LED is not lit.

These two lines of code, inside the endless loop, will cause the LED
corresponding to any particular switch to be lit as long as the switch is pushed. You can
press any number of switches simultaneously and the corresponding LED’s will be lit.

Now, we need to compile this program. A section of


the tool bar is shown to the left. The two buttons shown are
the ‘Compile’ button and the ‘Make’ button. The ‘Compile’
button does just that – it compiles the source file. Click on
this to see what happens. You should get a notice, shown
next, telling you that the program has compiled successfully
with no errors and no warnings. The notice also tells you
how much RAM memory has been used, the size of the
stacks and how much space has been allocated for global variables. It also causes the
assembly language file to be generated and stored on disk. In this case, since the C
program was named tutor1.c, the assembly language file which is generated is named
tutor1.asm.

If you clicked the ‘Make’ button instead of


the ‘Compile’ button, you would again cause the
compiler to operate and to generate the assembly
language file but you would also subsequently
invoke the assembler which then generates the
object code either as an Intel hex file or some other
format. We’ll discuss selecting these options a
little later. For now, for this project, the option has
been set to generate an Intel hex file which will be
named tutor1.hex. This may now be loaded into
the target 90S2313 microcontroller with the AVR
programming tool named AVRProg.exe; or
whichever other programmer you use.

The assembly language .asm file is


generated in a format compatible with the AVR
assembler. This is a very simple assembler with limited macro capabilities but
sophistication is not required here – the compiler does all the hard work. After
programming is complete, the program will run by itself and you can now light any LED
by pressing the corresponding switch on the AVRDB. You can change the target
processor from a 90S2313 to a 90S4414 or a 90S8515 without changing any of the code
and the program will still work correctly because the addresses assigned to the various

7
8

registers are the same for all these microcontrollers. If you have one of these, try it.
There will be one difference in the operation of the program and that is that the LED
corresponding to port B, bit 7 will not always be lit. Can you think why it is always lit
when the microcontroller is the 90S2313? Hint: the 90S2313 only has 15 I/O pins and
there are eight switches and eight LED’s – what difference does this make?

If you look at the structure of this very simple program, you will see an overall
structure which is very common in microcontroller programs. There is first a section
defining the attributes of the microcontroller being used; in this case, it is the four ‘srfb’
statements. This does not generate any code but instead allows the compiler to do its
work. Inside the body of the ‘main’ procedure, we have the code itself which is really in
two sections. The first two lines are the ‘initialization’ portion of the code which sets up
the microcontroller to do its task. Inside the endless loop, is the action which the
microcontroller is supposed to do. This basic structure, an initialization sequence
followed by an endless loop, is the quintessential microcontroller program.

How can we improve this simple program? Firstly, we shouldn’t have to look up
the addresses of the particular registers in order to write the ‘srfb’ statements. Instead,
we’d normally like to do this sort of definition with an ‘include’ file. We can do this with
the Codevision compiler and the author of the compiler has thoughtfully provided
‘include’ files for all the common AVR microcontrollers. To see how this works, just
edit the code in the tutor1.c file by removing the four ‘srfb’ statements and replacing
them with the single statement:

#include <90s2313.h>

Remember that C is case sensitive so type the line exactly as shown. 90s2313.h is a file
in the ‘inc’ folder of the cvavr directory. Try compiling the program again. It should
give you exactly the same result. You will notice that the number of lines which were
compiled is greater because this include file contains many more register definitions than
just the four we had in the original program. However, the compiled code will be exactly
the same length because the definitions generate no code.

You can open the ‘include’ file and look at it with the editor if you want to refresh
your memory about what registers are available, etc. Note that, using the convention
common to AVR programmers, all the registers use upper-case letters exclusively. You
must be careful not to accidentally reuse some of these names for variables in your
program.

Finally, let’s see what happens when we have errors in the code. For the purposes
of this exercise, let’s delete the last D of the name, PIND, in the first of the two lines
inside the endless loop. This line will become:

data=PIN;

8
9

Now, when you click the ‘Compile’ button, you will see that there is an error and a
warning. At the bottom of the compiler window, there is a white area which will now
contain two lines. The first of these will tell us what the error is and the second will
describe the warning. The warning is simple; we defined PIND with an ‘sfrb’ statement
but never used it. The error statement is a bit more obscure but still understandable – we
haven’t defined the name PIN but we’ve used it in a statement. If we click on this error
statement, the offending line will be highlighted in the edit window. Sure enough, it is
the line that we altered. If we alter it back again, the program will compile correctly.

Note that we didn’t have to save the source file anywhere along the way. When
the ‘compile’ button is clicked, the source file is saved automatically. This is terribly
convenient but also can cause problems. If you have a source file which works and you
want to make extensive changes in it, you really should save it somewhere first with a
different name. That way, your changes won’t overwrite an already working program.

Finally, let’s do one more thing before closing this project and that is to modify
the program to turn out that annoying bit 7 LED which is always on. If you haven’t
figured it out, let me just give you the solution which is to replace the line:

data=PIND;

with

data=PIND | 0x80;

Then, remake the program and reprogram it into the AVRDB and you’ll see that the last
bit 7 LED is always off. Finally, to make an even shorter program, we can replace the
two line program inside the while loop with just a single statement:

PORTB = PIND | 0x80;

Doing this, we have a program which now needs no declared variables so we can
eliminate the declaration of the unsigned char, data.

Now, close the project in the editor giving you a blank window for the next
exercise.

9
10

Projects, variables

A ‘project’ is just a convenient way of keeping together a group of files for a real,
physical project. It also provides a convenient way of specifying the tools needed for a
project and the exact configuration for that project. We’ll start a new project in this
exercise. During the course of working with this project, we’ll examine how the
compiler handles the three types of memory in a 90S series microcontroller: RAM,
EEPROM and flash (ROM).

First, you will want to create a new source file so, using the File | New pull down,
select Source file as the type you want to create and a text window will now open up. At
this point, it is useful to add something to the file while it is open so I usually just write
an opening comment describing the project, giving the date, etc. Then save the file using
Save As and give it any
appropriate name.

Now, we will create a


project using this source file.
Use File | New again but this
time, select the ‘Project’ radio
button because you wish to
start a new project. You will
be asked to give this project a
name – I opted to call this
tutor2. You will get a new
window like the one on the
left. Click the Add button and
select the source file which
you’ve just created as the one
to add to this project.

Clicking the compiler


tab gives you access to the
properties of the
microcontroller you will use
for this particular project. For
the example which we will do,
select the AT90S2313. The
default options for this
processor are 128 bytes of
RAM (this is determined by the
microcontroller itself) and a
stack size of 32. The top of the
hardware stack is normally

10
11

selected to be the top of RAM and that is the case for this compiler. The hardware stack
is the stack used by the microcontroller during subroutine calls, pushes and pops. The
data stack is used by the compiler for storing variables and you will want to set it to some
value less than the total ram size – in the example shown, I have set it to 96 bytes leaving
a hardware stack of 32 bytes. The quantities can be modified later if necessary. When a
program is compiled, the little box showing the compilation summary shows how the
variables are allocated in the data stack.

Clicking the
Assembler tab gives you
access to the properties of the
assembler output. If you’re
using AvrProg.exe as your
programming tool, it is most
convenient to choose the Intel
HEX format for the assembler
output. This is shown to the
left.

Finally, click on the


OK button to close this
window. You have now
created a project with a
source file. In any real life project, one normally will want to make extensive notes
associated with various aspects of the project and have them attached in some way to the
programming itself. These may contain details of the hardware development, problems
encountered, etc. Along with the source file in a project, the IDE automatically creates a
text file with the project name and the file extension of .txt. This file is useful for making
these notes in the course of developing a project.

For this project, we will expand on the very simple program used previously in
order to introduce some different concepts. The aim of this project is to make a system
very similar to the previous one except that, after pressing a switch, the LED stays lit
until another switch is pressed. There are probably a hundred ways to do this and it is
even possible that a competent (??) C programmer could do it in one very convoluted
statement. For the purposes of illustration, it is coded in a very formal way as shown in
the next text box.

In this simple example, we will not run out of memory space and speed will not
matter so we can indulge ourselves in the matter of style even though it is overkill for
such a simple program!

Here, the program is written in the form of an initialization procedure followed by


the endless while loop. Inside the loop, the program reads the switch port. If the byte is
not $FF (i.e., if a switch has been pressed) AND if the switch pressed is different from

11
12

the previous one, the value saved as the


/*
Revised version of tutor1.c in which the LEDs stay on after global variable ‘data’ is changed and it
release of the switch is written to the LED port. When this
*/
program is compiled, you get the
#include <90s2313.h> following compilation summary:
//
// global character declarations
//

unsigned char data; // global byte giving last switch press

//
// Prototype declarations
//

void initialize(void);
unsigned char read_switch_bank(void);
void write_to_LEDs(unsigned char ch);

//
// main program
//

void main(void)
{
unsigned char ch;

initialize();
You can see that global variables are
while (1) allocated space at the bottom of the
{
hardware stack and therefore reduce
ch = read_switch_bank(); hardware stack space. The data stack is
if ((ch != data) && (ch != 0xff)) // see if it has changed
{ reserved for local variables like the
data = ch; unsigned char variable, ch, located in
write_to_LEDs(data);
} the ‘main’ procedure. When compiling
} large programs, you should use this
}
compilation summary to keep an eye on
// how much room you have for the
// procedure and function definitions
// hardware stack. You can alter this at
any time by using the Projects |
void initialize(void)
{ Configure pull-down menu to change
the size of the data stack for the
data = 0xff; // starting value
DDRD = 0x00; // all inputs particular project.
DDRB = 0xff; // all outputs
PORTB = 0xff; // start by turning all LEDs off
} We have seen how the compiler
allocates variables in RAM. How does
unsigned char read_switch_bank(void)
{ it allocate variables in the flash memory
unsigned char ch; (ROM)? Since flash memory cannot be
ch = PIND | 0x80; // the $80 makes bit 7 a logical 1 altered in the course of a running
return (ch); program, flash variable storage is only
}
useful for constants. A common
void write_to_LEDs(unsigned char ch) example might be the strings which are
{
PORTB = ch; to be displayed by a running program on
} an LCD display. We can invoke this

12
13

kind of variable by using the keyword flash in the declaration as follows:

flash char SignOnMsg[] = “Greetings”;


flash char ErrorMsg[] = “Error # “;
flash int OneThousandPi = 3142;

and so on. The keyword flash ensures that the compiler will put the constant into flash
memory space.

As an example of this, let’s modify the preceding program so that the


microcontroller, when turned on, displays alternate LED’s lit. We may do this by adding
a line just after the global declaration as follows:

flash unsigned char TurnOn = 0xaa;

And, in the initialization procedure, initialize(), we change the first line to read:

data = TurnOn;

and the last line to read:

PORTB = data;

Now, the program will cause the byte $AA to be stored into the LED’s when first turned
on and then subsequently operate as before.

Of course, this is a trivial use of the flash keyword since we could have just as
easily just loaded the variable, data, directly with the value of 0xaa in the first line of
initialize(). Nevertheless, it does illustrate how one may use flash memory to store
constants. In a complex program, it makes sense to declare constants at the front of the
program and not write them into the program itself; it is easier to make changes later
without having to search through the whole text.

EEPROM can be accessed in exactly the same way by writing the keyword
eeprom before the otherwise normal declaration of a variable. The compiler produces all
the code necessary for the storage and retrieval of variables from eeprom. This can be
illustrated by modifying the program one more time so that, when turned on, it
‘remembers’ the last switch reading and comes up showing that LED lit. We’ll start by
changing the declaration for the byte, data, to:

eeprom unsigned char data = 0xaa;

The compiler will then allocate memory space in the EEPROM for this byte and will
generate an EEPROM *.eep file with the initial value stored in it. You may declare
arrays or any type of variable to be of type eeprom. If you do not assign initial values,
the compiler will generate a warning.

13
14

In the initialization procedure, we will not need to initialize the byte, data, as it
has the initial value given to it by the .eep file. It will retain its value when the power is
off so the next time the power is turned on, it will have the last stored value. To
summarize, we just need to remove the first statement in the initialize() procedure
leaving:

DDRD = 0x00;
DDRB = 0xff;
PORTB = data;

Now, when we program the microcontroller, we will also need to program the EEPROM
with the generated .eep file. When running, the program will now store the value of data
into EEPROM and, when turned on, load the initial value from the EEPROM. A
cautionary note: Atmel warns that the first byte in the EEPROM array is occasionally
corrupted during a power-off/power-on sequence. Since the compiler allocates EEPROM
variables in the order that they are declared, it is prudent to first declare an EEPROM
variable which is never used in the program to occupy the first byte. This will result in a
compiler warning which can be ignored.

Data hiding is considered to be one aspect of good programming and we can do


that by saving well-designed subroutines and functions in a library somewhere and just
‘INCLUDEing’ them in the source code. This compiler always looks for files given in a
#INCLUDE statement in the \inc sub-directory. In the program we have here, we can
‘cut’ the procedures and functions declared after the main() procedure and open a new
file and save them there. This can then be given a name (such at “tutorial_stuff.h”) and
saved in the \inc directory. In the main program, then, we somewhere just need to write a
statement:

#include <tutorial_stuff.h>

to give the compiler access to it. The program will compile normally and generate
exactly the same code as before.

14
15

Assembly Interface

Assembly code is used for one or more of three reasons: speed, compactness or
because some functions are easier to do in assembler than in a higher level language. It is
well known that using a high level language always results in the faster program
development but there are times when, for the reasons stated above, one wants to use
assembly language.

The CodeVisionAVR C Compiler, like other compilers meant for microcontroller


development, has an easy interface to assembly language. Assembler code may be
imbedded anywhere in a C program by using the following ‘inline’ construct:

#asm
….
….
Assembly language code
….
….
#endasm

The only precaution one must observe is to use only registers r4 through r20 inclusive; a
total of 17 registers are thus available for assembly language use. The other 15 registers
are used by the compiler and using of some of them in the inline assembler code might
compromise the rest of the program. The use of assembly language in a C program is
described in the CodeVisionAVR C Compiler help files. We will start with a very simple
example code here. Let’s suppose we want to reproduce the very first program used in
the project, tutor1 but we want to execute the read switches, write to LED loop as quickly
as possible. We will get this speed writing the program mostly in assembly language.
So, start a new project (use the default settings for data stack, etc.) which we will call
tutor3 and create a source file, also named tutor3.c which contains the code shown next.
Here, the skeleton structure is in C but the whole main() procedure is written in assembly
language in order to execute it as rapidly as possible.

This is a very simple program but it does illustrate the main features:

1. you have to give the assembler any used addresses with .equ statements. It doesn’t
help to use #include <90s2313> because those sfrb statements are instructions to the
compiler, not the assembler.
2. do not use r0 through r3 and r21 through r31 – any other registers are OK

Note the label used in the infinite loop here. The compiler creates labels for addresses
that it uses when it is creating an assembly language file. However compiler labels

15
16

always start with the underline character.


/*
For this reason, it behooves one not to use
Tutor3 program - reproduces the tutor1 program the underline as a first character in a label.
but does it as quickly as possible. The program is very
simple:
In this example, we have let the
- an initialization section
- an infinite loop of read switch, transfer to LED compiler create all the code necessary for
- setting up the hardware and software
*/
stacks. It will also fill the RAM data space
void main(void) with zeroes before jumping to the
{
beginning of the main() procedure. None
#asm of these are necessary for this simple
;
; program but might be in a more
; first,we have to give the assembler the addresses of the ports complicated program. Finally, it goes
; then, the initialization section
; without saying that one has to conform to
; we'll use register r16 throughout the syntax for the AVR Assembler – a very
;
; simple assembler.
.equ DDRB= $17
.equ DDRD= $11
.equ PORTB= $18 ; the output LED port Let’s do our next project as one
.equ PIND= $10 ; the input switch port where we want to use assembly language
;
; initialize because it allows us to do something which
; is difficult to do in a higher level language
ldi r16,0
out DDRD,r16 and that is to generate a well defined time
; set PORTD to all inputs
ldi r16,$ff delay. In C, we can always generate a time
out DDRB,r16 ; set PORT B to all outputs
; delay by simple for (i=0; i< n; i++) loops
; now, for the infinite loop but we are never sure of how long it will
;
forever_loop: take. In a low level language like
in r16,PIND assembly, we can calculate exactly how
; read the switches
ori r16,$80 ; set bit 7
out PORTB,r16 long a sequence of code will take.
; store result into LED port
rjmp forever_loop Consider the following problem. Suppose
#endasm
} we want to write a program in which there
are a series of delays each of which is some
integral number of milliseconds long (this is better done by using interrupts as we shall
see in the next section). For now, we’ll do it this way as an illustration of how to
interface to assembly
language. Let’s start by
Instruction Number of Cycles
writing a procedure which
ldi r16,n 1 will take precisely one
waitLoop:
ld r17,x 2 \ millisecond to execute.
ld r17,x 2 |
.... |
.... m lines = 2m cycles Consider the code in
ld r17,x 2 | the adjacent box. Here we
ld r17,x 2 |
ld r17,x 2 / have a tight loop which
dec r16 1 contains a number of
brne waitloop 2 except for the last time when it is one
statements which load r17
Total time = 1 + n(2m + 3) -1 = 2nm + 3n cycles with whatever is located
where the X register is

16
17

pointing. We have chosen this instead of a string of NOP’s because a NOP only takes
one cycle while the load r17 statements take two cycles each. As this will be written as a
subroutine, there will be a RET instruction at the end (4 cycles) and, of course, the calling
program will have to execute a ‘rcall’ statement which takes 3 cycles. The total time, in
cycles, for this procedure is thus n(2m+3) + 7 cycles. For the AVRDB, the crystal clock
runs at 4 MHz so each cycle is 0.25 microseconds. To generate a total delay of 1
/*
millisecond, we will therefore need the
Tutor4 - a program to blink the LED array at one total number of cycles to be 4000.
second intervals
Therefore, we want n(2m+3) = 3993.
*/ In order to conserve space, we also
want m to be as few as possible. The
#include <90s2313.h> other constraint is that n must be less
void WaitAMilliSec(void);
than 256 because it is a single byte.
Suppose m were 8, and n were 210,
void main(void)
{
then the total number of cycles would
be 3990 – just 3 short of what we
int i; // our counter
want. We can add 3 NOP’s after the
// loop and before the RET in order to
// initialize PORT B to all outputs
//
make the delay exactly equal to 4000
DDRB = 0xff; cycles.
//
// light every other LED
// So … let’s start a new project
PORTB = 0xaa;
and write a program which will blink
while (1) the LED’s once per second using our
{
for (i=0; i < 1000; i++) WaitAMilliSec(); // call it 1000 times
accurate one millisecond procedure.
PORTB = ~PORTB; // invert the LED display My version is shown in the adjacent
}
}
box. When this is compiled and
loaded into the ‘2313, you will see the
void WaitAMilliSec(void)
{
lights blink alternatively at
// approximately once per second. I’m
// use r16 and r17 since they are not used in the compiler
//
sure you can guess why I said
approximately. Even though we’ve
#asm
ldi r16, 210
gone to a lot of trouble to make the
wait_loop: subroutine take exactly one
ld r17,x ; do this eight times
ld r17,x
millisecond, the problem is that there
ld r17,x is some overhead in the for (……..)
ld r17,x
ld r17,x
loop and that is going to mean that the
ld r17,x lights will blink a little more slowly
ld r17,x
ld r17,x
than once per second. If you examine
dec r16 the .asm or .lst files which the
brne wait_loop
nop
compiler produces, you will see that
nop the overhead is only a few instructions
nop
; no RET needed
but those few instructions will add
#endasm several microseconds to each
}
millisecond and we will not have the
accuracy we’ve gone to so much

17
18

trouble to obtain.

The solution is to write a procedure, in assembly language, to which we pass the


number of milliseconds we want to delay. The prototype declaration will be:

void WaitMilliSecs(int number);

#pragma warn- With this procedure, we can delay


void WaitMilliSecs(int n) an accurate second by calling it with
{
// n is passed on the y stack. The MSB will be located at y+1 a parameter of 1000, or half a second
// and the LSB will be located at y with 500, and so on. To write this
// we'll use the r18, r19 pair as counters, r18 = MSB, r19 = LSB
// we'll set them to negative of the count and then increment procedure, we will need to know how
// up to zero variables are passed to functions or
#asm
clr r18 procedures. There is a file in the
clr r19 CodeVisionAVR C Compiler help
ld r17,y ; get LSB from stack
sub r19,r17 ; put negative value in r19 system which describes this in some
ldd r17,y+1 ; get MSB from stack detail. Parameters are passed to
sbc r18,r17 ; put negative into r18
inner_loop: functions and procedures on the data
rcall wait1msec ; takes precisely 1 mS to return stack. The pointer to this stack is the
inc r19 ; least sig byte - one cycle
brne inner_loop ; two cycles except the last time Y index register pair and the stack
inc r18 ; most sig byte builds downward. Parameters are
brne inner_loop
#endasm stacked in order of declaration within
} the brackets and are stacked MSB
#pragma warn+
first, then LSB. Procedures by
#asm definition have no return value but
;
; subroutine _wait1msec takes exactly one millisec functions do. Functions are returned
; use r16 and r17 since they are not used in the compiler in specific registers: char variables
;
wait1msec: are returned inr30, int and unsigned
ldi r16, 210 int variables are returned in the r30,
wait1msec_loop:
ld r17,x ; do this eight times r31 register pair with r30 having the
ld r17,x LSB and r31 the MSB. Long signed
ld r17,x
ld r17,x and unsigned int’s are returned in
ld r17,x four registers as follows: LSB – r30,
ld r17,x
ld r17,x next most – r31, next most – r22,
ld r17,x MSB - r23. In the procedure we will
dec r16
brne wait1msec_loop write, we will not return any values
nop so we do not need to use these. The
nop
nop code in the adjacent box (just the
ret procedure) shows how we recover
#endasm
the passed parameter in this case.

18
19

Strictly speaking, this solution is also not quite exact. Firstly, there is some
overhead in calling the WaitMilliSecs procedure from the calling program. Secondly,
there is some overhead inside this procedure – both in recovering the variables from the
stack and in the loop which calls the very precise 1 mS delay subroutine. However, these
latter two have delays which can be calculated and compensated for – we won’t bother to
go through that as it is tedious; it is possible, though. Then, we just have the overhead
from the calling program to contend with and this will just be a few cycles – a few µS.
Since the crystal clock in a typical microcontroller is rarely more accurate than one part
in 105, a few µS in a delay of several tens or hundred of mS is not significant. It might be
a problem if we needed accurate delays of just a few mS.

/*
We will leave this problem not quite
Test program for unsigned long integer increment solved. It was intended as an exercise in passing
routine done in assembly language. variables to an assembly language procedures
*/
unsigned long int number; and functions rather than a discussion of timing
unsigned long int IncLong(unsigned long int n); accuracy.
void main(void)
{ As a semi-final exercise, let’s look at
number = 1234567;
number = IncLong(number); passing variables back to the calling program.
while (1); Imagine a program where we want to increment
}
an unsigned long integer variable as quickly as
#pragma warn- possible - perhaps because it is often called in a
unsigned long int IncLong(unsigned long int i)
{ program. Incrementing a four-byte variable in C
#asm involves a four byte addition and so takes some
;
; time; we will be able to do it much faster in
; the stack will look like this: assembly language. The prototype function will
;
; MSB be:
; 2nd MSB
; 3rd MSB
; LSB <- Y unsigned long int IncLong(unsigned long int i);
;
; with Y pointing to the least significant byte
; of the variable The code for this function and its calling program
; is shown in the adjacent box. Because we’ll be
ldd r23,y+3 ; get MSB into r23
ldd r22,y+2 ; next into r22 returning the function value in registers r22, r23,
ldd r31,y+1 ; next into r31 r30 and r31, we can use these registers in the
ld r30,y ; least into r30
inc r30 ; increment LSB body of the code without worrying about the fact
brne done_IncLong that the compiler also uses them – the compiler
inc r31
brne done_IncLong will be expect them to be changed. We use the
inc r22 #pragma warn- before the function and #pragma
brne done_IncLong
inc r23 warn+ after so that the compiler will not generate
done_IncLong: a warning. It would normally do so since it does
;
; the results are already in the proper registers so not see a ‘return(i);’ statement anywhere in the
; we can just return function.
;
; NOTE - we do NOT alter the data stack point, Y
; Finally, there is one other topic worth
#endasm
} considering and that is accessing global variables
#pragma warn+ in assembly language sections. The

19
20

CodeVisionAVR C Compiler assigns memory locations in RAM to global variables. The


compiler uses the same name you give to the variable except that an underline character
is prefixed to the name. For example, if you have a list of global variables in a program
such as:

char ch, temp;


unsigned int value;

the compiler will generate assembly code which reserves storage in RAM with the names
_ch, _temp, and _value. Multi-byte variables such as ‘value’ in the list above, are stored
with the least significant byte at the lowest address and successively more significant
bytes at successive addresses. Because these global variables have been assigned storage
space by name, we may access them in assembly language directly. For example, given
the previous declarations, if we wanted to load a register, say r17, with the character, ch,
we could do it with an assembly language statement:

lds r17, _ch

Similarly, if we wanted to store a register, for example, again, r17, into the most
significant byte of the integer variable, value, we could do it with a statement like:

sts _value+1, r17

Please note that this technique only works with global variables. If you have internal
variables inside a procedure or function, they are stored on a stack and hence not
accessible by name.

20
21

Interrupts

The interrupt structure of the AVR 90S series of microcontrollers is completely


vectored. The first few locations in the flash memory are reserved for vectors to the
various interrupt service routines. In assembler, a program normally starts with the
sequence:

rjmp reset ; where it goes on RESET


rjmp INT0 ; where it goes if external interrrupt 0 line is activated
…….
…….

and so on. Each of these instructions takes a single word in flash memory and the
number of possible vectors depends on the processor; there are 11 for the 90S2313 and
13 for the 90S8515, for example.

To make use of these interrupt vectors, the compiler allows one to write special
interrupt procedures which are designated by writing the reserved word, interrupt, before
the procedure definition. The formal syntax is:

interrupt [vector number] void procedure_name(void)

where the vector number is the one given in the Atmel data sheets. These numbers start
with 1; vector 1 is the RESET vector. For example, vector number 3 is for the external
interrupt request 1, INT1. In general, the numbers differ for each microcontroller so,
when writing an interrupt service procedure, you need to be sure you’ve chosen the right
one for the particular microcontroller you are using. Because the interrupt service routine
is never called by anything, don’t waste too much time trying to think of a neat name for
it; any old name will do.

At the beginning of an interrupt procedure, the compiler automatically inserts


code to push all the registers it uses before starting the code and automatically pops them
before returning via an RETI instruction. This means that you do not have to worry
about saving anything.

In programs which use interrupts, the basic structure of the main() procedure is
the following:

- initialize the microcontroller normally


- set up the registers which control the interrupt(s) being used
- enable interrupts
- do the infinite loop which is the main program

The author of the compiler has written an example of code illustrating the use of
interrupts procedures in a project in the Examples\led directory. You should examine this

21
22

code. In the example, led.c, program, there is really no main program but just an empty
while() loop – all the work being done is done inside the interrupt service routine. A
more common way of handling interrupts is to have the interrupt service routines
communicate with the main program through global variables or ‘semaphores’. In this
mode, the main program runs in the ‘foreground’ and doesn’t know anything about the
interrupts. The interrupt service routines just change the value of some global variables
to indicate that an interrupt has occurred.

/* In the next exercise, we will change the


program above to explore the use of
Tutor 7 project - interrupt procedures
interrupts in this manner. The project,
*/ tutor7.prj, is a rewrite of led.prj in
// I/O register definitions which all the initialization is gathered
#include <90s2313.h> together into one procedure. The
#define fmove 2
#define xtal 4000000 tutor7.c code is shown at the left. I have
removed all the comment statements to
unsigned char led_status=0xfe;
make the code a little smaller – refer to
void initialize(void); Example\led.c to see what each
void main(void) statement does. In tutor7.c, the global
{ variable, led_status, is altered by the
initialize(); interrupt service routine but nothing else
is done. In the main program, the value
while (1)PORTB=led_status;
} of led_status is written to the LED bank.
Compile and ‘make’ this file and
interrupt [6] void timer1_overflow(void)
{ download it into the AVRDB to verify
TCNT1=0x10000-(xtal/1024/fmove); that it works in exactly the same way as
led_status+=led_status;
led_status|=1; led.prj does.
if (led_status==0xff) led_status=0xfe;
}
Now, let’s add another interrupt
void initialize(void) routine to change the pattern of the
{
DDRB=0xff; display if one of the external interrupts
PORTB=led_status; is activated. The push-button switches
DDRD=0xff;
PORTD=0; of the AVRDB are connected to PORT
D and two of the pins in PORT D can be
TCCR1A=0;
TCCR1B=5; set to be external interrupts. Let’s
TCNT1=0x10000-(xtal/1024/fmove); choose the INT 0 interrupt which is
TIFR=0;
TIMSK=0x80; activated by bit 2 of PORT D. We will
GIMSK=0; define a new global variable, called pb2,
#asm which has an initial value of zero and
sei which is inverted every time the
#endasm
interrupt occurs. We will also choose to
} activate the interrupt on the falling edge
of the signal on bit 2 of PORT D. The
code of this interrupt routine and the modified program are given in tutor8.prj; this is
shown below.

22
23

/* The program is very similar to


tutor7 except that another global
Tutor 8 project - interrupt procedures
variable has been added as described
*/ before. In the initialization procedure,
// I/O register definitions we also have to set up the INT0
#include <90s2313.h> interrupt characteristics. When this
#define fmove 2
#define xtal 4000000 program is running, generating a
general interrupt by pressing the push-
unsigned char led_status=0xfe;
unsigned char pb2 = 0; button switch (bit 2 of PORT D – this
is the third switch from the low end)
void initialize(void);
will cause the moving LED pattern to
void main(void) invert. This (i.e., the INT0 service
{
routine) is a very simple interrupt
initialize(); routine and, in real life, we would
while (1)if (pb2)PORTB=~led_status; else PORTB=led_status; want to debounce the switches or
} otherwise ensure that the interrupts
interrupt [2] void INT0(void) occurred controllably. In this simple
{ routine, because the switch is not
pb2 = ~pb2; // just invert it
} debounced, multiple interrupts can and
do occur and so the state of the
interrupt [6] void timer1_overflow(void)
{ variable, pb2, is somewhat
TCNT1=0x10000-(xtal/1024/fmove); indeterminate.
led_status+=led_status;
led_status|=1;
if (led_status==0xff) led_status=0xfe; For some microprocessors, you
}
have to disable the interrupts inside an
void initialize(void) interrupt routine to make sure that
{
DDRB=0xff; another interrupt cannot occur while
PORTB=led_status; you’re servicing an earlier one. In the
DDRD=0; // set PORT D to inputs
// timer 1 stuff case of the 90S series of
TCCR1A=0; microcontrollers, the internal interrupt
TCCR1B=5;
TCNT1=0x10000-(xtal/1024/fmove); enable flags are cleared when an
TIFR=0; interrupt is serviced thus preventing
TIMSK=0x80;
// INT0 stuff another interrupt from occurring
MCUCR=0x02; // set bit 1 to enable interrupt on falling edge unless you deliberately enable it in the
GIMSK=0x40; // enable interrupt on INT0
interrupt service routine code. The
#asm flags are reset when the RETI is
sei
#endasm executed at the end of the routine thus
re-enabling further interrupts.
}

23
24

Bit-wise I/O

Setting and clearing I/O bits in a microcontroller is not as simple as it may seem
at first glance. Consider the following case. Let’s imagine we have a controller with a
lot of I/O being handled by interrupt routines. Somewhere in the program, we want to
activate a relay which has been connected to bit 2 of PORTB. Let’s assume PORTB is a
general output port and the other pins go to other devices and let’s also assume that the
relay is activated by a logical zero and released by a logical 1. How do we do it?

Because we don’t know (or may not know) what the other pins on PORTB are
doing, we have to be sure that we don’t affect them. Therefore, we have to read the pins
of the port latch (PORTB) and then rewrite that same word back to the output latch,
PORTB, after making bit 2 a zero. The code to activate the relay would be something
like:

data = PORTB;
PORTB = data & 0xfb;

These two actions are shown here as separate statements to emphasize that it requires a
read of a register followed by a write of the register after some internal operations. What
happens if some of the other bits in the latch get changed in the time interval between
these two statements by an interrupt routine? The answer is that these pins will get put
back to what they were before the interrupt routine took place – it is as if the interrupt
never occurred. Note that changing the statement to:

PORTB = PORTB & 0xfb;

doesn’t help – the compiled code will still have a read of PORTB followed by a write to
PORTB at a subsequent time. To be safe, we’d have to turn the interrupts off before this
operation and then turn them back on after. This is awkward and may cause time-critical
interrupt routines to fail.

The 90S series of microcontrollers have machine code instructions for setting and
clearing individual bits in the I/O registers. This capability gets around the difficulty
described above but, unfortunately, standard C doesn’t have statements which allow the
use of these direct instructions. The CodeVisionAVR C Compiler has an extension to
standard C to make use of this capability. The instruction:

PORTB.2 = 0;

clears bit 2 of PORTB and doesn’t affect the other pins. The instruction:

PORTB.2 = 1;

24
25

sets bit 2 of PORTB and doesn’t affect the other pins. We can further clarify the code by
writing several ‘define’ statements such as:

#define relay_on PORTB.2=0


#define relay_off PORTB.2=1

Then, in our code for the program, to turn the relay on, we just need to write the
statement:

relay_on;

and, to turn it off, the statement:

relay_off;

This not only gets around the problem with changing a single bit, it also contributes to
writing clear, simple and easily understandable code.

We may also use single bit operations in reading ports. For example, in polling
the UART receive register to see if bit 7 were set indicating that a character has been
received by the UART. In a normal C program, we would have a statement like:

if (USR & 0x80) ………etc

There’s no problem with this but the function of the statement is not really very self-
evident. The CodeVisionAVR C Compiler allows a more concise code for doing the
same thing:

if (USR.7) …..etc

and, to be even clearer, we can define the RXC bit being set in a statement like:

#define character_received USR.7

and, the ‘if’ statement can be written:

if (character_received) ……..etc

NOTE: this syntax, for both single bit write and single bit read, is only allowed for the
I/O registers – these are register 0 through 31 inclusive. You cannot use this kind of
single bit operation, for example, to read from or write to the general interrupt mask
register, GIMSK, which is register number 59 (0x3b).

25

Potrebbero piacerti anche