Hello LED on an AVR (ATtiny45) in C
This article is part of a series.
- Part 1: This Article
- Part 2: How can I make programming an ARM chip as hard as possible?
- Part 3: How can I get this SAMD21E18 startup code a little sturdier?
- Part 4: It's ALIVE! (SAMD21E18A, Assembly, No SDK)
- Part 5: And now for 3 ways to set an internal pullup
- Part 6: It'd make sense to do some toolchain clean up
- Part 7: Neat, switching to the ItsyBitsy just... works
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.
- AVR (ATtiny this post)
- Probably skip PIC
- SAM which are all 32 bit Arm Cortex
- probably straight to STM32 Also 32-bit Arm Cortex MCU
- Maybe ESP32
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
- https://docs.arduino.cc/learn/starting-guide/getting-started-arduino/
- https://itp.nyu.edu/physcomp/lessons/
- ICs & Micros general info playlist. (General topics. It’s old.) https://www.youtube.com/watch?v=1qQE5Xwe7fs&list=PLfbQcOtLPyIFqfXwXdpnFJ2EN2RuLUpUl
Arduino on a breadboard
- https://docs.arduino.cc/built-in-examples/arduino-isp/ArduinoToBreadboard/
- https://docs.arduino.cc/retired/hacking/hardware/building-an-arduino-on-a-breadboard/ (back in my day…)
DataSheets
- https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-2586-AVR-8-bit-Microcontroller-ATtiny25-ATtiny45-ATtiny85_Datasheet.pdf
- http://ww1.microchip.com/downloads/en/devicedoc/atmel-0856-avr-instruction-set-manual.pdf
- http://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-7810-Automotive-Microcontrollers-ATmega328P_Datasheet.pdf
- http://ww1.microchip.com/downloads/en/DeviceDoc/ATmega48A-PA-88A-PA-168A-PA-328-P-DS-DS40002061A.pdf
Misc Other
- http://eleccelerator.com/fusecalc/
- https://avrdudes.github.io/avrdude/7.3/avrdude.html
- https://www.microchip.com/en-us/application-notes
- https://www.microchip.com/en-us/products/microcontrollers-and-microprocessors/32-bit-mcus/applications-reference-designs-and-solutions
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.
- AVR® Microcontrollers Peripheral Integration, Quick Reference Guide (previous version with ATTiny85(V) on it)
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
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:
- LED Pin is on PB3 not PB1
- for the LED HIGH is OFF (pin output in sink mode, not source mode)
- Input pin uses internal pull up resistor (so none on board), switch closed to GROUND is turning on the LED.
- In the picture you’ll see some extra wires “to nowhere.” I make “psuedo headers” in my breadboards sometimes so I don’t have to keep checking the pin diagrams.
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 |
- * R4 not in picture. Using chip’s internal pull up in real circuit, but its important not to forget to set it
- † R3 is actually 1k in the picture. Yellow LEDs get bright.
- ** D1 not in picture. It’s in my power supply.
- Why caps in the power circuit
- 7805 Datasheet
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
}
}
}
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.
- Why use an external clock? Synchronize with another IC
- Why use a crystal? Higher reliability for real time behavior
- If staying internal, why stick with the 1MHz clock? (or go lower)
- Lower power
- Easy math (1MHz = once per 0.000001 seconds = every microsecond)
- Why not? Something happens faster. Communication protocols, audio, expensive math…
My little demo program isn’t doing anything fancy so 1MHz works for me.
More Info on Fuses
- https://developerhelp.microchip.com/xwiki/bin/view/products/mcu-mpu/8-bit-avr/structure/fuses/
- https://www.ladyada.net/learn/avr/fuses.html
- https://embedderslife.wordpress.com/2012/08/20/fuse-bits-arent-that-scary/
- http://eleccelerator.com/fusecalc/fusecalc.php?chip=attiny85
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.
- https://github.com/todbot/CtrlM/blob/master/firmware/Makefile
- https://github.com/todbot/blink1/blob/main/hardware/firmware/Makefile
- https://github.com/todbot/FreeM/blob/master/firmware/freem_v1a/Makefile
But here are some demos from AVR LibC
- https://github.com/avrdudes/avr-libc/blob/main/doc/examples/asmdemo/Makefile
- https://github.com/avrdudes/avr-libc/blob/main/doc/examples/demo/Makefile
- https://github.com/avrdudes/avr-libc/blob/main/doc/examples/stdiodemo/Makefile
- https://github.com/avrdudes/avr-libc/blob/main/doc/examples/twitest/Makefile
- https://github.com/avrdudes/avr-libc/blob/main/doc/examples/largedemo/Makefile
Today I’ll roll one to better match the commands already shown.
Some differences?
- uses the elf file format instead of the .out for the intermediary
- uses the target name rather than “a”
- makes a lss (listing file, I called it dump.txt earlier) file
- makes a map file (debugging map)
- can switch which target with
make TARGET=switch_control
- can compile and run in one step with which target with
make TARGET=switch_control program
- I left out everything about the fuses for now, but there is an erase function.
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.
- Part 1: This Article
- Part 2: How can I make programming an ARM chip as hard as possible?
- Part 3: How can I get this SAMD21E18 startup code a little sturdier?
- Part 4: It's ALIVE! (SAMD21E18A, Assembly, No SDK)
- Part 5: And now for 3 ways to set an internal pullup
- Part 6: It'd make sense to do some toolchain clean up
- Part 7: Neat, switching to the ItsyBitsy just... works