Programming ATmega328P

Note: code for this project is available here.

Introduction

For learning microcontrollers, Arduino is a good introduction to the higher level concepts and abilities of a microcontroller. However, directly programming with the microcontroller in C can provide a greater learning opportunity (while also being less portable).

However, an example of needing to use the Arduino setup would be the weather station, as it used an ESP8266 and is not programmable using this method.

This project was tested on an Arduino Nano with an ATmega328P and CH340 USB communications chip.

Programmer

One of the easier (and cheaper) programmers to use would be the USBasp, as it is open source and as such has a lot of clones with adapters to make flashing on Arduino platforms easier.

The USBasp can also update/flash the bootloader on the Arduino, as the bootloader makes flashing the Arduino possible. In the Arduino IDE, in tools set the programmer to USBASP and then select Burn Bootloader.

However, this setup unlike the Arduino setup uses the D11 to D13 pins (MOSI, MISO, SCK) when programming. The USBasp can always be disconnected after programming if those pins are needed (from the 10 pin to USBasp connection point, or disconnecting the 5V/3.3V power).

For better debugging, serial communication can be used with the same interface as the Arduino programmer (USB) or an I²C HD44780 display if there is audio work with fluctuations from the host USB (code). This display can also be tested using Arduino. An example of this setup is shown to the right.

Programming

The easiest way to rapidly program on Linux would to use a makefile, as a make program is all that is required in order to start programming the microcontroller. The required programs include make (to setup the compiler), avr-gcc (compiler), and avrdude (programmer).

The included makefile for this project builds the code in a folder and then flashes the microcontroller. It can also get the assembly code if needing to look at the preformance, along with outputs the size when programming the microcontroller.

Available commands include make program (compile and program), make compile (compile only) make size, make asm (for assembly code). Both the compile and program also will warn of errors when compiling.

makefile
SHELL		:= /bin/bash
PRG			:= serial_test
OBJ			:= $(PRG).o
MCU_TARGET	:= atmega328p
F_CPU		:= 16000000UL
OPTIMIZE	:= -O2
PRG_DIR		:= .
BUILD_DIR	:= build-$(MCU_TARGET)
LIBS		=

#Source: https://www.nongnu.org/avr-libc/user-manual/group__demo__project.html
#look in subdirectories for source
#VPATH		=
override CFLAGS := -g -Wall $(OPTIMIZE) -mmcu=$(MCU_TARGET) -DF_CPU=$(F_CPU)
override LDFLAGS := -Wl,-Map,$(BUILD_DIR)/$(PRG).map

#float adds ~1.5kB to code
PRINTF_LIB_MIN = -Wl,-u,vprintf -lprintf_min
PRINTF_LIB_FLOAT = -Wl,-u,vfprintf -lprintf_flt
PRINTF_LIB = $(PRINTF_LIB_MIN)
MATH_LIB = -lm
LDFLAGS += $(PRINTF_LIB) $(MATH_LIB)

CC			= avr-gcc
OBJCOPY		= avr-objcopy
OBJDUMP		= avr-objdump
OBJSIZE		= avr-size

all: $(BUILD_DIR)/$(PRG).elf lst text eeprom

program: $(BUILD_DIR)/$(PRG).hex size
	avrdude -p $(MCU_TARGET) -c usbasp -e -U flash:w:$(BUILD_DIR)/$(PRG).hex

compile: $(BUILD_DIR)/$(PRG).hex size

size:
	$(OBJSIZE) -C --mcu=$(MCU_TARGET) $(BUILD_DIR)/$(PRG).elf

# $@ = filename of target, $< name of 1st prereq; $? name prereq newer, $^ all prereq
$(BUILD_DIR)/$(PRG).elf: $(OBJ)
	$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ $(LIBS)
	-mv *.o $(BUILD_DIR)/

clean:
	-rm -rf $(BUILD_DIR)/*.{o,elf,lst,map,srec,bin,hex,d}

# show headers + source + disasmb + line numbers + demangle
asm: $(BUILD_DIR)/$(PRG).elf
	$(OBJDUMP) -hSdlC $(BUILD_DIR)/$(PRG).elf > $(BUILD_DIR)/$(PRG).lst

lst: $(BUILD_DIR)/$(PRG).lst

$(BUILD_DIR)/%.lst: $(BUILD_DIR)/%.elf
	$(OBJDUMP) -h -S $< > $(BUILD_DIR)/$@

# Rules for building the .text rom images
text: hex bin srec

hex:  $(BUILD_DIR)/$(PRG).hex
bin:  $(BUILD_DIR)/$(PRG).bin
srec: $(BUILD_DIR)/$(PRG).srec

$(BUILD_DIR)/%.hex: $(BUILD_DIR)/%.elf
	$(OBJCOPY) -j .text -j .data -O ihex $< $@

$(BUILD_DIR)/%.srec: $(BUILD_DIR)/%.elf
	$(OBJCOPY) -j .text -j .data -O srec $< $@

$(BUILD_DIR)/%.bin: $(BUILD_DIR)/%.elf
	$(OBJCOPY) -j .text -j .data -O binary $< $@

# ======================= Other functions ===========================

SERIAL_PORT	= $(shell ls /dev/ttyUSB* | head -n 1)
COMM_PREF = .moserial.conf
serialcomm:
	sed -i 's,device='.*',device='"$(SERIAL_PORT)"',g' $(COMM_PREF)
	moserial --profile=$(COMM_PREF)

#Help from: https://collectiveidea.com/blog/archives/2017/04/05/arduino-programming-in-vim
#and https://www.avrfreaks.net/comment/2732811#comment-2732811
AVR_LIB = /usr/lib/avr/include
AVR_LIB_IO = /$(AVR_LIB)/avr
AVR_TAG = tags.avr
TAG_FILE = .tags
FILE_TYPES = -regex ".*\.\(h\|c\|ino\|hpp\|cpp\|pde\)"
ctags:
	find $(AVR_LIB) $(FILE_TYPES) -a -not -name "io*.h" -print > $(AVR_TAG)
	echo "$(AVR_LIB_IO)/io.h" >> $(AVR_TAG)
	echo "$(AVR_LIB_IO)/iom328p.h" >> $(AVR_TAG)
	find $(shell pwd) $(FILE_TYPES) -print >> $(AVR_TAG)
	ctags -L $(AVR_TAG) -f $(TAG_FILE)
	rm -f tags.*
 

Serial Monitor

For Linux based systems, the application moserial can be used. This allows serial communication to be send and received, and is quite easy to use. The command make serialcomm opens the serial monitor with the first /dev/ttyUSB* port found on the system.

For Windows, Tera Term, PuTTY, or the Arduino IDE can be used.

Other Resources

abcminiuser - AVRfreaks includes sample code that helped in the development of this code.

Oregon State ECE 473 (archive.org link) is a pretty good resource on ATmega128 in specific along with I²C, SPI, UART, timers, schematics, pushbutton debounce code, interrupts and fuses. This can be used in addition to the manual for an AVR microcontroller. There is also a sample makefile available.