At this point, you should have the GNU tools configured,
built, and installed on your system. In this chapter, we present a
simple example of using the GNU tools in an AVR project. After
reading this chapter, you should have a better feel as to how the
tools are used and how a Makefile
can be
configured.
This project will use the pulse-width modulator
(PWM
) to ramp an LED on and off every two
seconds. An AT90S2313 processor will be used as the controller.
The circuit for this demonstration is shown in the schematic diagram. If you have a
development kit, you should be able to use it, rather than build
the circuit, for this project.
Meanwhile, the AT90S2313 became obsolete. Either use its successor, the (pin-compatible) ATtiny2313 for the project, or perhaps the ATmega8 or one of its successors (ATmega48/88/168) which have become quite popular since the original demo project had been established. For all these more modern devices, it is no longer necessary to use an external crystal for clocking as they ship with the internal 1 MHz oscillator enabled, so C1, C2, and Q1 can be omitted. Normally, for this experiment, the external circuitry on /RESET (R1, C3) can be omitted as well, leaving only the AVR, the LED, the bypass capacitor C4, and perhaps R2. For the ATmega8/48/88/168, use PB1 (pin 15 at the DIP-28 package) to connect the LED to. Additionally, this demo has been ported to many different other AVRs. The location of the respective OC pin varies between different AVRs, and it is mandated by the AVR hardware.
The source code is given in demo.c. For the sake of
this example, create a file called demo.c
containing this source code. Some of the more important parts of
the code are:
As the AVR microcontroller series has
been developed during the past years, new
features have been added over time. Even though
the basic concepts of the timer/counter1 are
still the same as they used to be back in early
2001 when this simple demo was written
initially, the names of registers and bits have
been changed slightly to reflect the new
features. Also, the port and pin mapping of the
output compare match 1A (or 1 for older devices)
pin which is used to control the LED varies
between different AVRs. The file iocompat.h
tries to
abstract between all this differences using some
preprocessor #ifdef
statements,
so the actual program itself can operate on a
common set of symbolic names. The macros defined
by that file are:
OCR
the name of the OCR
register used to control the PWM (usually either
OCR1 or OCR1A)
DDROC
the name of the
DDR (data direction register) for the OC output
OC1
the pin number of
the OC1[A] output within its port
TIMER1_TOP
the TOP value
of the timer used for the PWM (1023 for 10-bit PWMs,
255 for devices that can only handle an 8-bit
PWM)
TIMER1_PWM_INIT
the
initialization bits to be set into control register
1A in order to setup 10-bit (or 8-bit) phase and
frequency correct PWM mode
TIMER1_CLOCKSOURCE
the
clock bits to set in the respective control register
to start the PWM timer; usually the timer runs at
full CPU clock for 10-bit PWMs, while it runs on a
prescaled clock for 8-bit PWMs
ISR() is a macro that marks the function as an interrupt routine. In this case, the function will get called when timer 1 overflows. Setting up interrupts is explained in greater detail in <avr/interrupt.h>: Interrupts.
The PWM
is being
used in 10-bit mode, so we need a 16-bit
variable to remember the current value.
This section determines the new value
of the PWM
.
Here's where the newly computed value
is loaded into the PWM
register. Since we are in an interrupt routine,
it is safe to use a 16-bit assignment to the
register. Outside of an interrupt, the
assignment should only be performed with
interrupts disabled if there's a chance that an
interrupt routine could also access this
register (or another register that uses
TEMP
), see the appropriate
FAQ entry.
This routine gets called after a
reset. It initializes the PWM
and enables interrupts.
The main loop of the program does
nothing -- all the work is done by the interrupt
routine! The sleep_mode()
puts the
processor on sleep until the next interrupt, to
conserve power. Of course, that probably won't
be noticable as we are still driving a LED, it
is merely mentioned here to demonstrate the
basic principle.
Early AVR devices saturate their outputs at rather low currents when sourcing current, so the LED can be connected directly, the resulting current through the LED will be about 15 mA. For modern parts (at least for the ATmega 128), however Atmel has drastically increased the IO source capability, so when operating at 5 V Vcc, R2 is needed. Its value should be about 150 Ohms. When operating the circuit at 3 V, it can still be omitted though.
This first thing that needs to be done is compile
the source. When compiling, the compiler needs to know the
processor type so the -mmcu
option is
specified. The -Os
option will tell the
compiler to optimize the code for efficient space usage (at the
possible expense of code execution speed). The
-g
is used to embed debug info. The debug
info is useful for disassemblies and doesn't end up in the
.hex files, so I usually specify it. Finally, the
-c
tells the compiler to compile and stop
-- don't link. This demo is small enough that we could compile
and link in one step. However, real-world projects will have
several modules and will typically need to break up the building
of the project into several compiles and one link.
The compilation will create a
demo.o
file. Next we link it into a binary
called demo.elf
.
It is important to specify the MCU type when linking.
The compiler uses the -mmcu
option to choose
start-up files and run-time libraries that get linked together.
If this option isn't specified, the compiler defaults to the
8515 processor environment, which is most certainly what you
didn't want.
Now we have a binary file. Can we do anything useful
with it (besides put it into the processor?) The GNU Binutils
suite is made up of many useful tools for manipulating object
files that get generated. One tool is
avr-objdump
, which takes information from
the object file and displays it in many useful ways. Typing the
command by itself will cause it to list out its options.
For instance, to get a feel of the application's
size, the -h
option can be used. The output of
this option shows how much space is used in each of the sections
(the .stab and
.stabstr sections hold the
debugging information and won't make it into the ROM file).
An even more useful option is -S
.
This option disassembles the binary file and intersperses the
source code in the output! This method is much better, in my
opinion, than using the -S
with the compiler
because this listing includes routines from the libraries and
the vector table contents. Also, all the "fix-ups" have been
satisfied. In other words, the listing generated by this option
reflects the actual code that the processor will run.
Here's the output as saved in the
demo.lst
file:
avr-objdump
is very useful, but
sometimes it's necessary to see information about the link that
can only be generated by the linker. A map file contains this
information. A map file is useful for monitoring the sizes of
your code and data. It also shows where modules are loaded and
which modules were loaded from libraries. It is yet another view
of your application. To get a map file, I usually add
-Wl,-Map,demo.map
to my link
command. Relink the application using the following command to
generate demo.map
(a portion of which is shown
below).
Some points of interest in the
demo.map
file are:
The .text segment (where program
instructions are stored) starts at location 0x0.
The last address in the .text segment is
location
0x114
( denoted by
_etext
), so the instructions use up 276
bytes of FLASH.
The .data segment (where initialized static
variables are stored) starts at location
0x60
,
which is the first address after the register bank on an ATmega8
processor.
The next available address in the .data
segment is also location
0x60
, so the
application has no initialized data.
The .bss segment (where uninitialized data
is stored) starts at location
0x60
.
The next available address in the .bss
segment is location 0x63, so the application uses 3 bytes of
uninitialized data.
The .eeprom segment (where EEPROM variables
are stored) starts at location 0x0.
The next available address in the .eeprom
segment is also location 0x0, so there aren't any EEPROM
variables.
We have a binary of the application, but how do we
get it into the processor? Most (if not all) programmers will
not accept a GNU executable as an input file, so we need to do a
little more processing. The next step is to extract portions of
the binary and save the information into .hex files.
The GNU utility that does this is called
avr-objcopy
.
The ROM contents can be pulled from our project's binary and put into the file demo.hex using the following command:
The resulting demo.hex
file
contains:
The -j
option indicates that we want
the information from the .text and
.data
segment extracted. If we specify the EEPROM segment, we can
generate a
.hex file that can be used to program the
EEPROM:
There is no demo_eeprom.hex
file
written, as that file would be empty.
Starting with version 2.17 of the GNU binutils, the
avr-objcopy
command that used to generate
the empty EEPROM files now aborts because of the empty input
section .eeprom, so these empty files are not
generated. It also signals an error to the Makefile which will
be caught there, and makes it print a message about the empty
file not being generated.
Rather than type these commands over and over, they
can all be placed in a make file. To build the demo project
using make
, save the following in a file called
Makefile
.
This Makefile
can only be
used as input for the GNU version of
make
.
PRG = demo OBJ = demo.o #MCU_TARGET = at90s2313 #MCU_TARGET = at90s2333 #MCU_TARGET = at90s4414 #MCU_TARGET = at90s4433 #MCU_TARGET = at90s4434 #MCU_TARGET = at90s8515 #MCU_TARGET = at90s8535 #MCU_TARGET = atmega128 #MCU_TARGET = atmega1280 #MCU_TARGET = atmega1281 #MCU_TARGET = atmega1284p #MCU_TARGET = atmega16 #MCU_TARGET = atmega163 #MCU_TARGET = atmega164p #MCU_TARGET = atmega165 #MCU_TARGET = atmega165p #MCU_TARGET = atmega168 #MCU_TARGET = atmega169 #MCU_TARGET = atmega169p #MCU_TARGET = atmega2560 #MCU_TARGET = atmega2561 #MCU_TARGET = atmega32 #MCU_TARGET = atmega324p #MCU_TARGET = atmega325 #MCU_TARGET = atmega3250 #MCU_TARGET = atmega329 #MCU_TARGET = atmega3290 #MCU_TARGET = atmega32u4 #MCU_TARGET = atmega48 #MCU_TARGET = atmega64 #MCU_TARGET = atmega640 #MCU_TARGET = atmega644 #MCU_TARGET = atmega644p #MCU_TARGET = atmega645 #MCU_TARGET = atmega6450 #MCU_TARGET = atmega649 #MCU_TARGET = atmega6490 MCU_TARGET = atmega8 #MCU_TARGET = atmega8515 #MCU_TARGET = atmega8535 #MCU_TARGET = atmega88 #MCU_TARGET = attiny2313 #MCU_TARGET = attiny24 #MCU_TARGET = attiny25 #MCU_TARGET = attiny26 #MCU_TARGET = attiny261 #MCU_TARGET = attiny44 #MCU_TARGET = attiny45 #MCU_TARGET = attiny461 #MCU_TARGET = attiny84 #MCU_TARGET = attiny85 #MCU_TARGET = attiny861 OPTIMIZE = -O2 DEFS = LIBS = # You should not have to change anything below here. CC = avr-gcc # Override is only needed by avr-lib build system. override CFLAGS = -g -Wall $(OPTIMIZE) -mmcu=$(MCU_TARGET) $(DEFS) override LDFLAGS = -Wl,-Map,$(PRG).map OBJCOPY = avr-objcopy OBJDUMP = avr-objdump all: $(PRG).elf lst text eeprom $(PRG).elf: $(OBJ) $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ $(LIBS) # dependency: demo.o: demo.c iocompat.h clean: rm -rf *.o $(PRG).elf *.eps *.png *.pdf *.bak rm -rf *.lst *.map $(EXTRA_CLEAN_FILES) lst: $(PRG).lst %.lst: %.elf $(OBJDUMP) -h -S $< > $@ # Rules for building the .text rom images text: hex bin srec hex: $(PRG).hex bin: $(PRG).bin srec: $(PRG).srec %.hex: %.elf $(OBJCOPY) -j .text -j .data -O ihex $< $@ %.srec: %.elf $(OBJCOPY) -j .text -j .data -O srec $< $@ %.bin: %.elf $(OBJCOPY) -j .text -j .data -O binary $< $@ # Rules for building the .eeprom rom images eeprom: ehex ebin esrec ehex: $(PRG)_eeprom.hex ebin: $(PRG)_eeprom.bin esrec: $(PRG)_eeprom.srec %_eeprom.hex: %.elf $(OBJCOPY) -j .eeprom --change-section-lma .eeprom=0 -O ihex $< $@ \ || { echo empty $@ not generated; exit 0; } %_eeprom.srec: %.elf $(OBJCOPY) -j .eeprom --change-section-lma .eeprom=0 -O srec $< $@ \ || { echo empty $@ not generated; exit 0; } %_eeprom.bin: %.elf $(OBJCOPY) -j .eeprom --change-section-lma .eeprom=0 -O binary $< $@ \ || { echo empty $@ not generated; exit 0; } # Every thing below here is used by avr-libc's build system and can be ignored # by the casual user. FIG2DEV = fig2dev EXTRA_CLEAN_FILES = *.hex *.bin *.srec dox: eps png pdf eps: $(PRG).eps png: $(PRG).png pdf: $(PRG).pdf %.eps: %.fig $(FIG2DEV) -L eps $< $@ %.pdf: %.fig $(FIG2DEV) -L pdf $< $@ %.png: %.fig $(FIG2DEV) -L png $< $@