A simple project(3)

NAME
A simple project - 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.

The Project

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.

 Schematic of circuit for demo projectSchematic of circuit for demo project

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:

Note [1]:.RS 4 The PWM is being used in 10-bit mode, so we need a 16-bit variable to remember the current value.

Note [2]:.RS 4 SIGNAL() is a macro that marks the function as an interrupt routine. In this case, the function will get called when the timer overflows. Setting up interrupts is explained in greater detail in Interrupts and Signals.

Note [3]:.RS 4 This section determines the new value of the PWM.

Note [4]:.RS 4 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.

Note [5]:.RS 4 This routine gets called after a reset. It initializes the PWM and enables interrupts.

Note [6]:.RS 4 The main loop of the program does nothing -- all the work is done by the interrupt routine! If this was a real product, we'd probably put a SLEEP instruction in this loop to conserve power.

Note [7]:.RS 4 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.

The Source Code

/*
 * ----------------------------------------------------------------------------
 * 'THE BEER-WARE LICENSE' (Revision 42):
 * <joerg@FreeBSD.ORG> wrote this file.  As long as you retain this notice you
 * can do whatever you want with this stuff. If we meet some day, and you think
 * this stuff is worth it, you can buy me a beer in return.        Joerg Wunsch
 * ----------------------------------------------------------------------------
 *
 * Simple AVR demonstration.  Controls a LED that can be directly
 * connected from OC1/OC1A to GND.  The brightness of the LED is
 * controlled with the PWM.  After each period of the PWM, the PWM
 * value is either incremented or decremented, that's all.
 *
 * $Id: demo.c,v 1.4.2.1 2005/04/09 14:55:28 joerg_wunsch Exp $
 */
#include <inttypes.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/signal.h>
#if defined(__AVR_AT90S2313__)
#  define OC1 PB3
#  define OCR OCR1
#  define DDROC DDRB
#elif defined(__AVR_AT90S2333__) || defined(__AVR_AT90S4433__)
#  define OC1 PB1
#  define DDROC DDRB
#  define OCR OCR1
#elif defined(__AVR_AT90S4414__) || defined(__AVR_AT90S8515__) ||       defined(__AVR_AT90S4434__) || defined(__AVR_AT90S8535__) ||       defined(__AVR_ATmega163__)
#  define OC1 PD5
#  define DDROC DDRD
#  define OCR OCR1A
#elif defined(__AVR_ATmega8__)
#  define OC1 PB1
#  define DDROC DDRB
#  define OCR OCR1A
#  define PWM10 WGM10
#  define PWM11 WGM11
#elif defined(__AVR_ATmega32__) || defined(__AVR_ATmega16__)
#  define OC1 PD5
#  define DDROC DDRD
#  define OCR OCR1A
#  define PWM10 WGM10
#  define PWM11 WGM11
#elif defined(__AVR_ATmega64__) || defined(__AVR_ATmega128__)
#  define OC1 PB5
#  define DDROC DDRB
#  define OCR OCR1A
#  define PWM10 WGM10
#  define PWM11 WGM11
#else
#  error 'Don't know what kind of MCU you are compiling for'
#endif
#if defined(COM11)
#  define XCOM11 COM11
#elif defined(COM1A1)
#  define XCOM11 COM1A1
#else
#  error 'need either COM1A1 or COM11'
#endif
enum { UP, DOWN };
volatile uint16_t pwm; /* Note [1] */
volatile uint8_t direction;
SIGNAL (SIG_OVERFLOW1) /* Note [2] */
{
    switch (direction) /* Note [3] */
    {
        case UP:
            if (++pwm == 1023)
                direction = DOWN;
            break;
        case DOWN:
            if (--pwm == 0)
                direction = UP;
            break;
    }
    OCR = pwm; /* Note [4] */
}
void
ioinit (void) /* Note [5] */
{
    /* tmr1 is 10-bit PWM */
    TCCR1A = _BV (PWM10) | _BV (PWM11) | _BV (XCOM11);
    /* tmr1 running on full MCU clock */
    TCCR1B = _BV (CS10);
    /* set PWM value to 0 */
    OCR = 0;
    /* enable OC1 and PB2 as output */
    DDROC = _BV (OC1);
    timer_enable_int (_BV (TOIE1));
    /* enable interrupts */
    sei ();
}
int
main (void)
{
    ioinit ();
    /* loop forever, the interrupts are doing the rest */
    for (;;) /* Note [6] */
        ;
    return (0);
}

Compiling and Linking

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.

    $ avr-gcc -g -Os -mmcu=at90s2333 -c demo.c

The compilation will create a demo.o file. Next we link it into a binary called demo.elf.

    $ avr-gcc -g -mmcu=at90s2333 -o demo.elf demo.o

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.

Examining the Object File

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.

    $ avr-objdump -h -S demo.elf > demo.lst

Here's the output as saved in the demo.lst file:

demo.elf:     file format elf32-avr
Sections:
Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         000000cc  00000000  00000000  00000094  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .data         00000000  00800060  000000cc  00000160  2**0
                  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000003  00800060  00800060  00000160  2**0
                  ALLOC
  3 .noinit       00000000  00800063  00800063  00000160  2**0
                  CONTENTS
  4 .eeprom       00000000  00810000  00810000  00000160  2**0
                  CONTENTS
  5 .stab         000005e8  00000000  00000000  00000160  2**2
                  CONTENTS, READONLY, DEBUGGING
  6 .stabstr      00000704  00000000  00000000  00000748  2**0
                  CONTENTS, READONLY, DEBUGGING
Disassembly of section .text:
00000000 <__vectors>:
   0:	0a c0       	rjmp	.+20     	; 0x16 <__ctors_end>
   2:	63 c0       	rjmp	.+198    	; 0xca <__bad_interrupt>
   4:	62 c0       	rjmp	.+196    	; 0xca <__bad_interrupt>
   6:	61 c0       	rjmp	.+194    	; 0xca <__bad_interrupt>
   8:	60 c0       	rjmp	.+192    	; 0xca <__bad_interrupt>
   a:	5f c0       	rjmp	.+190    	; 0xca <__bad_interrupt>
   c:	5e c0       	rjmp	.+188    	; 0xca <__bad_interrupt>
   e:	5d c0       	rjmp	.+186    	; 0xca <__bad_interrupt>
  10:	07 c0       	rjmp	.+14     	; 0x20 <__vector_8>
  12:	5b c0       	rjmp	.+182    	; 0xca <__bad_interrupt>
  14:	5a c0       	rjmp	.+180    	; 0xca <__bad_interrupt>
00000016 <__ctors_end>:
  16:	11 24       	eor	r1, r1
  18:	1f be       	out	0x3f, r1	; 63
  1a:	cf ed       	ldi	r28, 0xDF	; 223
  1c:	cd bf       	out	0x3d, r28	; 61
  1e:	4f c0       	rjmp	.+158    	; 0xbe <main>
00000020 <__vector_8>:
volatile uint16_t pwm; /* Note [1] */
volatile uint8_t direction;
SIGNAL (SIG_OVERFLOW1) /* Note [2] */
{
  20:	1f 92       	push	r1
  22:	0f 92       	push	r0
  24:	0f b6       	in	r0, 0x3f	; 63
  26:	0f 92       	push	r0
  28:	11 24       	eor	r1, r1
  2a:	2f 93       	push	r18
  2c:	8f 93       	push	r24
  2e:	9f 93       	push	r25
    switch (direction) /* Note [3] */
  30:	80 91 60 00 	lds	r24, 0x0060
  34:	99 27       	eor	r25, r25
  36:	00 97       	sbiw	r24, 0x00	; 0
  38:	19 f0       	breq	.+6      	; 0x40 <__SREG__+0x1>
  3a:	01 97       	sbiw	r24, 0x01	; 1
  3c:	31 f5       	brne	.+76     	; 0x8a <__SREG__+0x4b>
  3e:	14 c0       	rjmp	.+40     	; 0x68 <__SREG__+0x29>
    {
        case UP:
            if (++pwm == 1023)
  40:	80 91 61 00 	lds	r24, 0x0061
  44:	90 91 62 00 	lds	r25, 0x0062
  48:	01 96       	adiw	r24, 0x01	; 1
  4a:	90 93 62 00 	sts	0x0062, r25
  4e:	80 93 61 00 	sts	0x0061, r24
  52:	80 91 61 00 	lds	r24, 0x0061
  56:	90 91 62 00 	lds	r25, 0x0062
  5a:	8f 5f       	subi	r24, 0xFF	; 255
  5c:	93 40       	sbci	r25, 0x03	; 3
  5e:	a9 f4       	brne	.+42     	; 0x8a <__SREG__+0x4b>
                direction = DOWN;
  60:	81 e0       	ldi	r24, 0x01	; 1
  62:	80 93 60 00 	sts	0x0060, r24
  66:	11 c0       	rjmp	.+34     	; 0x8a <__SREG__+0x4b>
            break;
        case DOWN:
            if (--pwm == 0)
  68:	80 91 61 00 	lds	r24, 0x0061
  6c:	90 91 62 00 	lds	r25, 0x0062
  70:	01 97       	sbiw	r24, 0x01	; 1
  72:	90 93 62 00 	sts	0x0062, r25
  76:	80 93 61 00 	sts	0x0061, r24
  7a:	80 91 61 00 	lds	r24, 0x0061
  7e:	90 91 62 00 	lds	r25, 0x0062
  82:	89 2b       	or	r24, r25
  84:	11 f4       	brne	.+4      	; 0x8a <__SREG__+0x4b>
                direction = UP;
  86:	10 92 60 00 	sts	0x0060, r1
            break;
    }
    OCR = pwm; /* Note [4] */
  8a:	80 91 61 00 	lds	r24, 0x0061
  8e:	90 91 62 00 	lds	r25, 0x0062
  92:	9b bd       	out	0x2b, r25	; 43
  94:	8a bd       	out	0x2a, r24	; 42
  96:	9f 91       	pop	r25
  98:	8f 91       	pop	r24
  9a:	2f 91       	pop	r18
  9c:	0f 90       	pop	r0
  9e:	0f be       	out	0x3f, r0	; 63
  a0:	0f 90       	pop	r0
  a2:	1f 90       	pop	r1
  a4:	18 95       	reti
000000a6 <ioinit>:
}
void
ioinit (void) /* Note [5] */
{
    /* tmr1 is 10-bit PWM */
    TCCR1A = _BV (PWM10) | _BV (PWM11) | _BV (XCOM11);
  a6:	83 e8       	ldi	r24, 0x83	; 131
  a8:	8f bd       	out	0x2f, r24	; 47
    /* tmr1 running on full MCU clock */
    TCCR1B = _BV (CS10);
  aa:	81 e0       	ldi	r24, 0x01	; 1
  ac:	8e bd       	out	0x2e, r24	; 46
    /* set PWM value to 0 */
    OCR = 0;
  ae:	1b bc       	out	0x2b, r1	; 43
  b0:	1a bc       	out	0x2a, r1	; 42
    /* enable OC1 and PB2 as output */
    DDROC = _BV (OC1);
  b2:	82 e0       	ldi	r24, 0x02	; 2
  b4:	87 bb       	out	0x17, r24	; 23
    timer_enable_int (_BV (TOIE1));
    /* enable interrupts */
    sei ();
}
int
main (void)
{
    ioinit ();
    /* loop forever, the interrupts are doing the rest */
    for (;;) /* Note [6] */
        ;
    return (0);
}
  b6:	84 e0       	ldi	r24, 0x04	; 4
  b8:	89 bf       	out	0x39, r24	; 57
  ba:	78 94       	sei
  bc:	08 95       	ret
000000be <main>:
  be:	cf ed       	ldi	r28, 0xDF	; 223
  c0:	d0 e0       	ldi	r29, 0x00	; 0
  c2:	de bf       	out	0x3e, r29	; 62
  c4:	cd bf       	out	0x3d, r28	; 61
  c6:	ef df       	rcall	.-34     	; 0xa6 <ioinit>
  c8:	ff cf       	rjmp	.-2      	; 0xc8 <main+0xa>
000000ca <__bad_interrupt>:
  ca:	9a cf       	rjmp	.-204    	; 0x0 <__heap_end>

Linker Map Files

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).

    $ avr-gcc -g -mmcu=at90s2313 -Wl,-Map,demo.map -o demo.elf demo.o

Some points of interest in the demo.map file are:

 *(.rela.plt)
 *(.vectors)
 .vectors       0x0000000000000000       0x16 ../../../build/crt1/crts2313.o
                0x0000000000000000                __vectors
                0x0000000000000000                __vector_default
                0x0000000000000016                __ctors_start = .

The .text segment (where program instructions are stored) starts at location 0x0.

 *(.fini2)
 *(.fini1)
 *(.fini0)
                0x00000000000000cc                _etext = .
                0x0000000000800060                PROVIDE (__data_start, .)
 *(.data)
 *(.gnu.linkonce.d*)
                0x0000000000800060                . = ALIGN (0x2)
                0x0000000000800060                _edata = .
                0x0000000000800060                PROVIDE (__data_end, .)
                0x0000000000800060                PROVIDE (__bss_start, .)
 *(.bss)
 *(COMMON)
 COMMON         0x0000000000800060        0x3 demo.o
                0x0000000000800060                direction
                0x0000000000800061                pwm
                0x0000000000800063                PROVIDE (__bss_end, .)
                0x00000000000000cc                __data_load_start = LOADADDR (.data)
                0x00000000000000cc                __data_load_end = (__data_load_start + SIZEOF (.data))
                0x0000000000800063                PROVIDE (__noinit_start, .)
 *(.noinit*)
                0x0000000000800063                PROVIDE (__noinit_end, .)
                0x0000000000800063                _end = .
                0x0000000000800063                PROVIDE (__heap_start, .)
 *(.eeprom*)
                0x0000000000810000                __eeprom_end = .

The last address in the .text segment is location 0xf2 ( denoted by _etext ), so the instructions use up 242 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 a 2313 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.

Intel Hex Files

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:

    $ avr-objcopy -j .text -j .data -O ihex demo.elf demo.hex

The resulting demo.hex file contains:

:100000000AC063C062C061C060C05FC05EC05DC046
:1000100007C05BC05AC011241FBECFEDCDBF4FC07B
:100020001F920F920FB60F9211242F938F939F93CD
:10003000809160009927009719F0019731F514C05D
:10004000809161009091620001969093620080938C
:10005000610080916100909162008F5F9340A9F4EC
:1000600081E08093600011C08091610090916200F6
:10007000019790936200809361008091610090915C
:100080006200892B11F410926000809161009091C0
:1000900062009BBD8ABD9F918F912F910F900FBEE3
:1000A0000F901F90189583E88FBD81E08EBD1BBC1B
:1000B0001ABC82E087BB84E089BF78940895CFEDB5
:0C00C000D0E0DEBFCDBFEFDFFFCF9ACF56
:00000001FF

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:

    $ avr-objcopy -j .eeprom --change-section-lma .eeprom=0 -O ihex demo.elf demo_eeprom.hex

The resulting demo_eeprom.hex file contains:

:00000001FF

which is an empty .hex file (which is expected, since we didn't define any EEPROM variables).

Make Build the Project

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.

Note:
    This Makefile can only be used as input for the GNU version of make.

PRG            = demo
OBJ            = demo.o
MCU_TARGET     = atmega8
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)
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 $< $@
%_eeprom.srec: %.elf
        $(OBJCOPY) -j .eeprom --change-section-lma .eeprom=0 -O srec $< $@
%_eeprom.bin: %.elf
        $(OBJCOPY) -j .eeprom --change-section-lma .eeprom=0 -O binary $< $@
# 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 $< $@