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.
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 "fuse registers". 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'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'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
Width | Signedness | Typedef | Const Cast |
---|---|---|---|
8 | signed | int8_t | INT8_C |
8 | unsigned | uint8_t | UINT8_C |
16 | signed | int16_t | INT16_C |
16 | unsigned | uint16_t | UINT16_C |
32 | signed | int32_t | INT32_C |
32 | unsigned | uint32_t | UINT32_C |
64 | signed | int64_t | INT64_C |
64 | unsigned | uint64_t | UINT64_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'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'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's clock rate.
- Set USART_BAUD for your desired baud rate.
usart.c
#include <stdio.h> #include <avr/io.h> #include "usart.h" /* Define baud rate */ #define USART_UBRR_VALUE (((F_CPU)/(8*USART_BAUD)) - 1) static int usart_send(char c, FILE *dummy){ if (c == ' ') usart_send('\\r', 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 == '\\r') c = ' '; /* 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'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!