AVR 8-bit Microcontrollers

This is a work-in-progress


Introduction

This will be my little blurb about programming 8-bit AVR microntrollers in C, what I learned, pitfalls, gotchas...

This guide is tailored to using the gcc-avr compiler and toolchain.


Toolchain

Install gcc-avr, binutils-avr, avr-libc, and avrdude. These packages are probably available through your package manager.

AVR GCC man page

Example Makefile

##################################################
# Compiler
##################################################
CC = avr-gcc
OBJCOPY = avr-objcopy
STRIP = avr-strip
OBJDUMP = avr-objdump
CFLAGS = -Os -std=gnu99 -fshort-enums -pedantic-errors -Wall -Werror 
	-Wstrict-prototypes -Wmissing-prototypes -Wcast-align -Wshadow

##################################################
# Files
##################################################
HDRS = file1.h file2.h
OBJS = file1.o file2.o
MAIN = main

##################################################
# Device
##################################################
MMCU = atmega16
MCU = m16
PROGRAMMER = avrisp2
PORT = usb

##################################################
# Global Targets
##################################################
all: $(MAIN).hex

clean:
	$(RM) *.o $(MAIN) *.asm *.hex io.inc

install: $(MAIN).hex
	avrdude -v 
	-p $(MCU) 
	-c $(PROGRAMMER) 
	-P $(PORT) 
	-U lfuse:w:0xEE:m 
	-U hfuse:w:0xD9:m 
	-U flash:w:$(MAIN).hex

##################################################
# Dependency Targets
##################################################
$(MAIN): $(OBJS)
	$(CC) -mmcu=$(MMCU) $(OBJS) -o $@

%.asm: %
	$(OBJDUMP) -S -d $< > $@

%.hex: %
	$(STRIP) $< -o $<-stripped
	$(OBJCOPY) -O ihex $<-stripped $@
	$(RM) $<-stripped

%.o: %.c $(HDRS) Makefile
	$(CC) -mmcu=$(MMCU) -c $< -o $@ $(CFLAGS)

Fuse Bits

The AVR microcontrollers have special registers known as &quot;fuse registers&quot;. The bits of these registers can control the clock sources, clock dividers, JTAG programming, and other stuff. The functionality of the fuse bits depends on your chip model. Consult your chip&apos;s documentation before setting any fuse bits.

Setting the Fuse Bits

Fuse bits must be set by the programmer, in this case, we are using avrdude to set the bits. From the example Makefile:

  • -U lfuse:w:0xEE:m sets the low fuse register to 0XEE
  • -U hfuse:w:0xD9:m sets the high fuse register to 0xD9

See the avrdude manual for usage instructions.


Size Matters

It does not take a very large program to fill-up your microntroller&apos;s program memory. A few printfs and divides can take up kilobytes of precious memory real-estate. That is why using the right size integers for storage and processing is so important.

Exact-Width Types

C provides a mess of keywords for dealing with sizes and signedness: signed, unsigned, char, short, int, long... These keywords make for unclean and especially unportable code. Fortunately, C also provides stdint.h which has a number of typedefs and macros for dealing with exact-width integers. The table below sumarizes some of the definitions in stdint.h

WidthSignednessTypedefConst Cast
8signedint8_tINT8_C
8unsigneduint8_tUINT8_C
16signedint16_tINT16_C
16unsigneduint16_tUINT16_C
32signedint32_tINT32_C
32unsigneduint32_tUINT32_C
64signedint64_tINT64_C
64unsigneduint64_tUINT64_C

Exact-Width Example

#include <stdint.h>

/*
 * 4 different ways to assign a constant value.
 */
uint32_t num0 = 0xffffffff;           //bad
uint32_t num1 = 0xfffffffful;         //messy
uint32_t num2 = UINT32_C(0xffffffff); //good
uint32_t num3 = (uint32_t)0xffffffff; //good
  • Assignment to num0: The compiler will truncate the number to the lower 2 bytes, and num1 will be initialized to 0xffff. This is not what we want!
  • Assignment to num1: The ul at the end of the number tells the compiler that this integer is an unsigned long. Since the actual width of a long can vary from compiler to compiler, this usage is unportable.
  • Assignment to num2: The const cast macro is used. This macro literally appends a ul to the end of the value. This is okay because the macro is deciding the correct postfix at compile time.
  • Assignment to num3: A typical C-style cast is used.

Standard IO

Standard IO is an excellent tool for debugging your program. Unfortunately, your microcontroller is probably not attached to a PC keyboard and monitor. So how can we use Standard IO? Use a serial connection between the AVR&apos;s USART and a PC with a serial-terminal program, like minicom.

The USART

Most (if not all) AVRs come with a USART. Standard IO can be used over your AVR&apos;s built-in USART. All you need is a function to send a byte, a function to receive a byte, and a call to fdevopen. After that, USART RX and TX effectively becomes stdin and stdout; you can use scanf, printf, and all the other various functions in stdio.h.

usart.h

#ifndef USART_H
#define USART_H

#include <stdint.h>

#define F_CPU UINT32_C(8000000) /* 8 Mhz */
#define USART_BAUD UINT32_C(9600) /* 9600 Baud */

void usart_init(void);

#endif /* USART_H */
  • Set F_CPU for your chip&apos;s clock rate.
  • Set USART_BAUD for your desired baud rate.

usart.c

#include <stdio.h>
#include <avr/io.h>
#include &quot;usart.h&quot;

/* Define baud rate */
#define USART_UBRR_VALUE (((F_CPU)/(8*USART_BAUD)) - 1)

static int usart_send(char c, FILE *dummy){
	if (c == &apos;
&apos;) usart_send(&apos;\\r&apos;, dummy); /* CR + LF */
	/* Wait if a byte is being transmitted */
	loop_until_bit_is_set(UCSRA, UDRE);
	/* Transmit data */
	UDR = c;
	return 0; /* success */
}

static int usart_recv(FILE *dummy){
	/* Wait until a byte has been received */
	loop_until_bit_is_set(UCSRA, RXC);
	/* Return received data */
	char c = UDR;
	if (c == &apos;\\r&apos;) c = &apos;
&apos;; /* CF -> LF */
	return (int) c;
}

void usart_init(void){
	/* Set baud rate */
	UBRRH = UINT8_C(USART_UBRR_VALUE >> 8);
	UBRRL = UINT8_C(USART_UBRR_VALUE >> 0);
	/* set the usart divider to 8 */
	UCSRA = _BV(U2X);
	/* Enable receiver and transmitter */
	UCSRB = _BV(RXEN) | _BV(TXEN);
	/* Set frame format to 8 data bits, no parity, 1 stop bit */
	UCSRC = _BV(URSEL) | _BV(UCSZ1) | _BV(UCSZ0);
	/* Setup stdio */
	fdevopen(usart_send, usart_recv);
}
  • USART registers can differ from model to model: Consult your chip&apos;s documentation on setting up the USART registers.
  • loop_until_bit_is_set and _BV are macros found in avr/io.h.
  • Make sure to call usart_init() in main() before making any use of stdio.

End

Go get yourself an AVR!

Last edited: Sat, Jun 5 2021 - 08:58PM