Hello LED on an AVR (ATtiny45) in C

This article is part of a series.

Related Repo: https://github.com/carlynorama/StrippedDownChipRosetta

I want to start using official Embedded Swift, but I was thinking I should have some reference code on some different board families in C handy first.

Turns out there’s a great tutorial series for AVR already done. Mitch Davis has a YouTube playlist:Fundamentals of Microcontrollers - Arduino bare-metal breakdown

The 9 part video series goes from blinking an LED on an Arduino board to blinking an LED with an ATTiny85 on a breadboard via linkers and optimizers and setting fuses and more. Assumes basic Arduino knowledge, but no C or other microcontroller experience. Because this series did such a good job covering what I would have wanted to cover I can just hop on down to where he leaves off by making an example with a switch and a Makefile.

One note: please unplug your boards when moving around wires.

More links below for supplemental reference.

Arduino 101

Arduino on a breadboard

DataSheets

Misc Other

A note about the ATtiny85 and doing things on a breadboard.

I used to teach in person so I have A LOT of ATtinyX5 dip package chips lying around. Most people these days would buy a dev board (e.g. Arduino Pro mini) that has a surface mount AVR. Probably one that runs at 3.3V. I still like the DIP socket boards for prototyping because the DIP chips are sturdier and easily replaceable. I have many products from both companies below.

The chip I’m actually using is the ATtiny45. It has half the memory of the 85. For the ATTiny the number after the “y” is the flash memory size in k (it could be “16”). The last number is an indicator of the number of pins, but but not literally. The ATtiny44 has more than the ATtiny45. There’s a whole new set of ATTiny’s now, too. Here’s a guide.

Check computer readiness before starting

All this presumes a machine that has compiled C and Swift, so it may be set up for clang but not avr-gcc. None of this will use an Arduino IDE’s install of these tools so it doesn’t matter if that’s on the box or not. Nor will any of this require software or libraries from ATMEL (now Microchip). Nor will the code import AVR LibC.

which avr-gcc

# points to 3rd party repo
# https://github.com/osx-cross/homebrew-avr
brew tap osx-cross/avr 

brew install avr-gcc
brew install avrdude

## Check install by printing out the compatible mcu's
## using zsh not bash escape the ?
avrdude -p \?

No need for avrdude’s -C (config file location) option. My 2024 homebrew install (version 7.3) doesn’t seem to need it.

Confirm compilation

Before I wired up the board, I checked that the code compiled and I got reasonable looking hex/assembly. My hello-world code doesn’t duplicate the tutorial exactly, but it’s pretty close.

This code will successfully blink a pin on PB3 (tested.)

//TODO: (blog meta) chroma treats #define as a comment bug report

//blink.c

//typically handled by AVR LibC
//10.4.2 PORTB – Port B Data Register
#define PORTB *((volatile unsigned char*) 0x38) //(0x20 + 0x18)
//10.4.3 DDRB – Port B Data Direction Register
#define DDRB *((volatile unsigned char*) 0x37)  //(0x20 + 0x17)

#define LED_PIN 3

//delay for the number of clock cycles
//approximately micros with default clock
//(version in AVR LibC)
void delay(volatile long time) {
    while (time != 0) {
        time--;
    }
}

int main() {
    DDRB |= (1<<LED_PIN); 
    while (1) {
        PORTB |= (1<<LED_PIN); // pin high
        delay(50000); //half a second at a 1MHz clock
        PORTB &= ~(1<<LED_PIN); // pin low
        delay(50000);
    }
}

How to compile, similar to video, with bonus how to get the assembly.

## (-g turns on debugging information, -Os optimizes)
avr-gcc -g -Os -mmcu=attiny45 blink.c

## leads to a.out which can be inspected in a hex editor 
## or save the interleaved disassembly and source with
avr-objdump -h -S a.out > dump.txt

## Make the ihex file for avrdude
avr-objcopy -O ihex -j.text -j.data a.out a.hex

blink_dump.txt

Wire Up the Board

The videos cover a basic version that will mostly work with the code above (LED inverted and on a different pin). The Arduino on a breadboard links at the top also cover a similar task.

Here’s a photo of my final set up because it’s a bit different. Links to a schematic and an annotated BOM.

Things to note:

pin diagram of the ATtiny

Image of the breadboard with the ATtiny, a power circuit, 2 LEDs, two tact switches and a mess of wires.

Schematic of the breadboard set up

schematic: kicad sch file

Bill of Materials

Annotated parts list.

Designator Description Qty Use
SW1, SW2 6mm Tact Switch with 5 mm Height 2 Reset, Digital Input
R2, R4* 10kΩ axial resistor 2 external pull up
R1, R3† 330Ω axial resistor 2 current limiter
J2 wires to external power. 1 external power source (7-16V)
J1 wires to AVR-ISP programmer 1 chip programming
D2 5.0mm LED (Red) 1 Power Indicator
D4 5.0mm LED (Yellow) 1 Digital Output
D1** 1N4004 1 protection diode
C2 10uF polarized electrolytic 1 input (higher voltage) side
C1 1uF polarized electrolytic 1 output (regulated voltage) side
U2 DIP-8 ATtinyX5 1 Microcontroller
U1 LM7805 TO-220 or T-92 fine 1 Voltage Regulator

Program the Board

The new code:

//10.4.2 PORTB – Port B Data Register
#define PORTB *((volatile unsigned char*) 0x38) //(0x20 + 0x18)
//10.4.3 DDRB – Port B Data Direction Register
#define DDRB *((volatile unsigned char*) 0x37)  //(0x20 + 0x17)
//10.4.4 PINB – Port B Input Pins Address
#define PINB *((volatile unsigned char *)0x36)  //(0x20 + 0x16)

#define LED_PIN 3
#define SWITCH_PIN 4

int main() {
    //set led pin bit to output
    DDRB |= (1<<LED_PIN);  
    
    //set switch pin bit to input
    DDRB &= ~(1<<SWITCH_PIN); 
     //for voltage high == no press config, sets pull up.
    PORTB |= (1<<SWITCH_PIN);
    //give the pull up a moment.
    
    //internal pull ups have high R and pin may be slow to reach logic high threshold. If problem, chip has to have settle time out of reset (SUT) or code does. 

    while (1) {
        if (!(PINB & (1<<SWITCH_PIN))) {  //if pressed
        //above if compares (0x000?0000 & 0x00010000) => 
        // - if true, seeing pull up. button not pressed
        // - if false, seeing ground via closed switch, button pressed
            PORTB &= ~(1<<LED_PIN);   // pin low, LED ON
        } else {
            PORTB |= (1<<LED_PIN);    // pin high, LED OFF 
        }
    }
}

switch_control_dump.txt

avrdude -p t45 -c usbtiny  #to confirm board is ready

## REDO compile with new code, & upload
avr-gcc -g -Os -mmcu=attiny45 switch_control.c
avr-objcopy -O ihex -j.text -j.data a.out a.hex
avrdude -p t45 -c usbtiny -U flash:w:a.hex:i

More on Fuses & Clock Settings

Video 5 of the series(“AVRDude and fuses”) talks about setting the fuses for the chip’s clock settings.

# memtype:operation:value:format
# m means direct value, as opposed to i for intel hex
avrdude -v -p t45 -c usbtiny -U lfuse:w:0xF2:m

I skipped that, but now I want to see the settings for the chip I grabbed.

# h is hexadecimal mode for the read.
# "each value will get the string 0x prepended."
avrdude -v -p t45 -c usbtiny -U hfuse:r:-:h -U lfuse:r:-:h

In the output will be:

avrdude: using SCK period of 10 usec

avrdude: processing -U hfuse:r:-:h
avrdude: reading hfuse memory ...
avrdude: writing output file <stdout>
0xdf 

avrdude: processing -U lfuse:r:-:h
avrdude: reading lfuse memory ...
avrdude: writing output file <stdout>
0x62

That means the registers look like:

 low fuse: 0110 0010
high fuse: 1101 1111

Both of these values are the defaults, and the clock information is discussed in section 6.2 of the datasheet. ( 1 is unset and 0 is set.)

7 6 5 4 3 2 1 0
low CKDIV8 CKOUT SUT1 SUT0 CKSEL3 CKSEL2 CKSEL1 CKSEL0
high RSTDISBL DWEN SPIEN WDTON EESAVE BODLEVEL2 BODLEVEL1 BODLEVEL0

This default setting means the chip is running at 1MHz (8MHz calibrated internal RC oscillator (6.2.3) with the CKDIV8 bit set).

The 0xF2 setting from the video clears the dividing bit (sets CKDIV8 to 1) and the SUT0 bit as well since it only matters with CKDIV8 is enabled.

Clock settings matter a lot. Here’s some tips.

My little demo program isn’t doing anything fancy so 1MHz works for me.

More Info on Fuses

Switching to a Make file

Running those compile and upload commands by hand gets old. Enter Makefiles, which I reviewed before.

For embedded projects I tend to cheat off of todbot.

But here are some demos from AVR LibC

Today I’ll roll one to better match the commands already shown.

Some differences?

I tend to have the Makefile to put build products in a separate folder, but since I’m undecided on how I’m going to organize these demo files I’m skipping that for now.

###############################################################################
# Makefile for a simple ATtiny project
###############################################################################

## General Flags
TARGET ?= blink #set if not defined 
MCU = attiny45

# for when using AVR LibC
#F_CPU = 16000000   # 16MHz on Arduino board
#F_CPU = 8000000    # internal osc
#F_CPU = 1000000    # internal osc w/ clkdiv8
#F_CPU  =  500000   # internal osc w/ clkdiv16
#F_CPU =  250000    # internal osc w/ clkdiv32

# Theses values are currently the defaults. 
LFUSE=0x62
HFUSE=0xDF
EFUSE=0xFF


CC = avr-gcc
CPP = avr-g++

## Options common to compile, link and assembly rules
COMMON = -mmcu=$(MCU)

## Compile options common for all C compilation units.
CFLAGS = $(COMMON)
CFLAGS += -Wall
CFLAGS += -g
CFLAGS += -Os

# for when using AVR LibC // i.e. #if F_CPU == 500000 type code
#Flags that start with -D are a define 
#equivalent to #define F_CPU = the variable value
#CFLAGS += -DF_CPU=$(F_CPU) #creates the F_CPU var for the compiler. 

## Linker flags
LDFLAGS = $(COMMON)
LDFLAGS +=  -Wl,-Map=$(TARGET).map

## Objects that must be built in order to link
OBJECTS = $(TARGET).o 

## Intel Hex file production flags
HEX_FLASH_FLAGS = -j.text -j.data 

## Objects explicitly added by the user
LINKONLYOBJECTS = 

# Programming support using avrdude. Settings and variables.
AVRDUDE = avrdude
AVRDUDE_PROGRAMMER = usbtiny
#AVRDUDE_PRODUCT = t45

AVRDUDE_WRITE_FLASH = -U flash:w:$(TARGET).hex:i

AVRDUDE_FLAGS = -p $(MCU) -c $(AVRDUDE_PROGRAMMER)

## Build
all: $(TARGET).elf $(TARGET).hex $(TARGET).lss size

## Compile
%.o : %.c
	$(CC) $(INCLUDES) $(CFLAGS) -c  $<

##Link
%.elf: $(OBJECTS)
	 $(CC) $(LDFLAGS) $(OBJECTS) $(LINKONLYOBJECTS) -o $(TARGET).elf

%.hex: $(TARGET).elf
	avr-objcopy -O ihex $(HEX_FLASH_FLAGS)  $< $@

%.lss: $(TARGET).elf
	avr-objdump -h -S $< > $@

size: ${TARGET}.elf
	@echo
	@avr-size -C --mcu=${MCU} ${TARGET}.elf

erase:
	$(AVRDUDE) $(AVRDUDE_FLAGS) -e -U lfuse:w:$(LFUSE):m -U hfuse:w:$(HFUSE):m -U efuse:w:$(EFUSE):m

# Program the device.  
program: $(TARGET).hex
	$(AVRDUDE) $(AVRDUDE_FLAGS) $(AVRDUDE_WRITE_FLASH)

## Clean target
#.PHONY: clean
clean:
	-rm -rf $(OBJECTS) $(TARGET).elf $(TARGET).hex $(TARGET).lss $(TARGET).map 

The Swift/clang ecosystem prefers CMake, which I don’t know very well at all (CMake hello world post).

I guess I’ll be learning!

This article is part of a series.