Intro
I've been learning electronics and microcontrollers. I've decided to pick the AVR family (which is what the Arduino development boards use) to dig deep and learn what is happening under the hood. I plan to eventually move to the ARM or RISCV microcontrollers, however it is recommended by many to start with 8bit processors especially when learning assembly.
I'm hoping by writing posts about what I've learned or solved I'll improve understanding myself. And hopefully help others with their journeys as well.
The purpose of this post is to give a very high level compare and contrast between the different abstractions. If you are a beginner trying to get an LED to blink for the first time, this post may help, but i'm writing from the perspective of someone curious what is under the hood and how it differs depending what level of abstraction is used.
Blink LED with Arduino
What is Arduino?
The term Arduino can mean different things depending on what the context is. Arduino can either mean the hardware ecosystem (eg. Arduino Uno Dev Board, or the shields and hardware add ons) , the software ecosystem (eg. the Arduino IDE), or the company itself.
It can easily get confusing because the software tooling has been expanded to be utilized by various other microcontrollers. The hardware shields can often be used with other microcontrollers.
You will frequently hear people use the term Arduino to describe their hardware development board. The Arduino development boards such as the Arduino UNO R3, are an easy to use package with a microcontroller and other peripherals on a printed circuit board (PCB). In the Arduino UNO R3's case, this chip is the ATmega328p which outside of being a common microcontroller for hobbyists, but is officially marketed as a commercial automotive microcontroller.
In this post I will be specific about what I'm referring to such as Arduino IDE, Arduino UNO, etc.
TLDR: The Arduino hardware and software ecosystem makes it easy to learn and create smart electronic devices without needing an engineering degree or many years of experience.
Blink LED with Arduino IDE
Getting started
This isn't a tutorial on getting your development environment set up such as installing the Arduino IDE, nor is it really a tutorial about getting your first LED to blink. For those purposes I suggest looking at the official documentation. I'll include a few links below.
Official Blink LED Tutorial
Installing IDE
After connecting my Arduino UNO R3, I'm going to open the Arduino IDE, create a new sketch, add my code, click the upload icon (arrow button). After that if everything is working correctly the code will compile, the program will then be copied from your computer to your device via USB, your device should reset and then run the code that is now on the device's flash memory.
Brief Explanation of code
The Arduino software ecosystem is a big abstraction over the underlying c/c++ code underneath. There is a preprocessor that adds things like header files, function prototypes, etc to make it a valid c++ program when it compiles.
The setup function runs once at power on (later on we'll see it also used for things like waking up from deep sleep). You will put your setup code such as setting pinMode, start your Serial communications, initialize sensors, etc. In this case you are telling the compiler that this pin will be used as output or to send electrical signals. In this case it's the pin tied to the builtin LED.
The loop function starts and if the end is reached it will repeat until the device is powered down, reset, etc. For this example we are sending an digital signal out on that pin (turning the builtin LED on), waiting a second (1000 milliseconds) so the LED remains on for a seconds, turning the digital signal off (and therefore the LED), waiting a second, and then the loops starts over.
That's it. The Hello World of microcontrollers is getting an LED to blink. Further on you will get a better idea of what is going on under the hood.
Blink LED with Arduino CLI
I personally prefer working and editing my code in a terminal instead of using the Arduino IDE. It reduces distractions, runs faster with less latency, and gets you closer to the actual toolchain and hardware. The below examples are for MacOS, Linux should be similar, Windows you will have to read the docs.
For the arduino-cli you will need to download the CLI tool. I've added a link below to help you get install and get started
arduino-cli installation
Once you install the Arduino CLI open up a terminal and navigate to your sketch. Or you can create a new sketch (file with .ino extension).
You can compile (which under the hood turns into valid c++ code, compiles, links, etc) by using the arduino-cli program with compile, and at the very least the fully qualified board name, and path to the sketch (in this case it's current directly so I will use a "." or dot).
To find your board name you can run the following command (I'm using grep to avoid some things specific to my laptop).
arduino-cli board list | grep arduino
/dev/cu.usbmodem141101 serial Serial Port (USB) Arduino Uno arduino:avr:uno arduino:avr
# The DOT is very important as it means use current directory
arduino-cli compile --fqbn arduino:avr:uno .
Once the compilation is complete you will need to upload your program to your development board. Use the device name and fqbn from the arduino-cli board list command above.
arduino-cli upload -p /dev/tty.usbmodem141101 --fqbn arduino:avr:uno .
This should allow you to now edit, compile, and upload your code without using the Arduino IDE. if you prefer you can mix and match (edit code using another tool like vim or VSCode, then compile and upload from Arduino IDE, etc) so you don't need to use 100% the cli or Arduino IDE.
Scripting and Automation
You may question if the CLI tools really save time or if it's really worth it. This is where writing a script or using a build tool really comes in handy.
You could write a batch file or PowerShell script if on Windows, you could write a shell script if Linux or Mac. I prefer to use Make which is available on Linux and Mac (there are similar tools for Windows). Make is very common in the C/C++ space and I find it handy as a general automation runner for other things.
You will need to install Make and create a Makefile. You can get fancy, you can add variables, create different sections, but I like to keep it as simple as possible and expand when needed.
# Showing contents of Makefile
cat Makefile
all:
arduino-cli compile --fqbn arduino:avr:uno .
arduino-cli upload -p /dev/tty.usbmodem141101 --fqbn arduino:avr:uno .
Now when I'm done editing code and need to compile and upload I have to... Type make.
make
arduino-cli compile --fqbn arduino:avr:uno .
Sketch uses 924 bytes (2%) of program storage space. Maximum is 32256 bytes.
Global variables use 9 bytes (0%) of dynamic memory, leaving 2039 bytes for local variables. Maximum is 2048 bytes.
Used platform Version Path
arduino:avr 1.8.6 /Users/dj/Library/Arduino15/packages/arduino/hardware/avr/1.8.6
arduino-cli upload -p /dev/tty.usbmodem141101 --fqbn arduino:avr:uno .
New upload port: /dev/tty.usbmodem141101 (serial)
That's it. I type in a single command and my Arduino code gets compiled and uploaded. I use this same approach for other languages/technologies as well.
Blink LED on AVR with C (no Arduino)
This can get confusing if you think of your Arduino UNO R3 as an Arduino instead of a hardware/software ecosystem. I'm going to blink the LED without using the Arduino software at all. Just C, open source toolchain, and the Arduino UNO R3 (AVR ATmega328p)
create a main.c (or whatever you want to call it).
#include <avr/io.h> //Lets me use names like DDRB, PORTB, instead of memory addresses
#include <util/delay.h> // Used for _delay_ms
int main(void) {
// Bitwise OR to set register bit to 1 which sets the pin as output
DDRB |= (1 << PB5); // Set PB5 (Arduino pin 13) as output
// Loop forever. This is equivalent to the Arduino loop() function
while (1) {
// Bitwise OR to set register bit to 1 which sets the pin high
PORTB |= (1 << PB5); // Equivalent to digitalWrite(13, HIGH);
_delay_ms(1000);
// Bitwise AND with NOT to set register bit to 0 which sets the pin low
PORTB &= ~(1 << PB5); // Equivalent to digitalWrite(13, LOW);
_delay_ms(1000);
}
return 0; // This line is never reached, but it's good practice to include it
}
Whoa, that looks a bit scary doesn't it? With time this isn't hard to read, but until then thinking about bits, bytes, bitwise operations takes some getting used to.
If you compare this with the Arduino code you notice this looks nothing alike. The Arduino software ecosystem hides a lot of the complexities. Which may be fine, but if you really need more control or just want a better understanding you have the ability to get under the hood.
Bitwise functions
Before you can understand the code you need to understand some basic bitwise functions. At a low level there is no easy function to enable something, or set a specific mode. It's all 1s and 0s at the end of the day.
What I struggled with initially is understanding why bitwise functions are needed. And the answer is it's a an easy way to turn specific bits into 0s or 1s. This may change when you step into the next level which is assembly where you are commonly working with actual memory addresses.
The |=
operator makes it easy to set a bit to 1. Because no matter what is in that memory address the output of an OR is 1.
The &= ~()
operation is an easy way to clear a bit to 0. As you are explicitly setting the output to zero (or NOT) and trying to AND it. This is always zero.
Another operation that is good to know despite not being used in this post (but I could have)is the `^=' operation which acts as a toggle. I could have used this instead of the above two operations to turn the LED on and off.
Explanation of code
At the top we include header files both of which are included when you install the toolchain. avr/io.h lets us use names instead of memory addresses. Working directly with memory addresses will be covered in the assembly section.
Up to 8 pins are tied to a port. This is due to the AVRs 8bit register size. So digital pin 13 once you dig under the hood is PortB pin 5 (6th pin since we start at 0). DDRB is is the direction register for PortB. So by setting the corresponding bit to 1 it turns that pin to output. I found Arduino pins are input by default when learning this. This is what setting a pin to OUTPUT in the Arduino code is doing behind the scenes.
If we move on to the while (1) block. This is the equivalent to the loop() function in Arduino which runs repeatedly if the end of the loop is encountered. The first line is saying take PORTB pin 5 and flip that bit to 1 (HIGH) which will start sending a digital signal (aka electricity which can power an LED). The delay function while different than Arduino is self explanatory. After 1 second (1000ms) we are going to turn the pin off but doing an &= ~()
which is ANDing a NOT (always zero) to turn the bit to zero and turn the pin off. Delay another second then repeat.
I've already shown what a Makefile is and why I use it in the previous section. So I'll just jump right into showing you my makefile.
MCU = atmega328p
F_CPU = 16000000UL
CFLAGS = -Wall -Os -mmcu=$(MCU) -DF_CPU=$(F_CPU)
TARGET = main
all: $(TARGET).hex
$(TARGET).elf: $(TARGET).c
avr-gcc $(CFLAGS) -o $@ $<
$(TARGET).hex: $(TARGET).elf
avr-objcopy -O ihex -R .eeprom $< $@
avrdude -v -patmega328p -carduino -P/dev/tty.usbmodem141101 -b115200 -D -Uflash:w:$(TARGET).hex:i
clean:
rm -f *.elf *.hex
Instead of arduino-cli you will now see avr-gcc (compiles and links), avr-objcopy (converts to a format the microcontroller understands), and avrdude ("uploads" or flashes the code to the microcontroller). Most of this is relatively self explanatory, but the big thing to note is F_CPU which is needed for the delay function in our code (delay is based of the clock speed).
make
avr-gcc -Wall -Os -mmcu=atmega328p -DF_CPU=16000000UL -o main.elf main.c
avr-objcopy -O ihex -R .eeprom main.elf main.hex
avrdude -v -patmega328p -carduino -P/dev/tty.usbmodem141101 -b115200 -D -Uflash:w:main.hex:i
Avrdude version 8.0
Copyright see https://github.com/avrdudes/avrdude/blob/main/AUTHORS
System wide configuration file is /usr/local/etc/avrdude.conf
User configuration file /Users/dj/.avrduderc does not exist
Using port : /dev/tty.usbmodem141101
Using programmer : arduino
Setting baud rate : 115200
AVR part : ATmega328P
Programming modes : SPM, ISP, HVPP, debugWIRE
Programmer type : Arduino
Description : Arduino bootloader using STK500 v1 protocol
HW Version : 3
FW Version : 4.4
AVR device initialized and ready to accept instructions
Device signature = 1E 95 0F (ATmega328P, ATA6614Q, LGT8F328P)
Reading 176 bytes for flash from input file main.hex
in 1 section [0, 0xaf]: 2 pages and 80 pad bytes
Writing 176 bytes to flash
Writing | ################################################## | 100% 0.06 s
Reading | ################################################## | 100% 0.03 s
176 bytes of flash verified
Avrdude done. Thank you.
Once again a single command will do all the necessary steps to take my source code and compile, link, upload, etc to my device. Makes editing and running code a fast and painless process.
There it is, a big step in complexity but also understanding in how this is working. Next up assembly.
Blink LED with AVR assembly (no Arduino)
Assembly used to intimidate me (before ever looking at what it was). It does read a lot different than high level languages but it's not nearly as bad I as expected. Learning assembly lets you start understanding how computers really work under the hood. You will use the instructions of the actual CPU. It's really cool stuff.
Just turning the LED on
To avoid scaring people away after talking about assembly, here is the code needed to turn on the LED (not blink). Its actually about the same as C if we just turn on the LED since really we just need to turn two bits on.
#define __SFR_OFFSET 0x00
#include
.org 0x0000
rjmp main
main:
sbi DDRB, PB5 ; Set PB5 as output
sbi PORTB, PB5 ; Set PB5 high
I'm using avr-gcc (gcc is a c compiler) which the toolchain includes an assembler which allows us to easily use the C preprocessor to include some definitions and header files. The definition is needed for the avr/io.h header we are importing below it which as used previously lets us use friendlier names like PB5 instead of a specific memory address (like we are using in the .org 0x0000 line).
.org 0x0000 is saying when reading the start of the memory where our program lives, jump to the main label.
Now onto the good stuff. The sbi (Set Bit in I/O Register) instruction is telling the CPU that we want to set the PB5 bit (PortB Pin5) in the DDRB register to set that GPIO pin as an output pin. Next we do the same by setting pin5 bit in the PortB register to turn on the LED.
This is really as bare metal as it gets. We could have used actual memory addresses for example the first line in main could be sbi 0x05, 5
but having friendly names is generally better.
Blinking the LED
This is it. This is bare metal AVR assembly to blink an LED. As you start to peel the abstractions away you can see both how much nicer the abstractions tend to be, but how much of what actually happens is hidden.
#define __SFR_OFFSET 0x00
#include
.org 0x0000
rjmp main
main:
sbi DDRB, PB5 ; Set PB5 as output
loop:
sbi PORTB, PB5 ; Set PB5 high
rcall delay_1s ; Call delay subroutine
cbi PORTB, PB5 ; Set PB5 low
rcall delay_1s ; Call delay subroutine
rjmp loop ; Repeat the loop
delay_1s:
ldi r18, 60 ; Load 60 into r18
first_loop:
ldi r19, 255 ; Load 255 into r19
second_loop:
ldi r20, 255 ; Load 255 into r20
third_loop:
nop
dec r20
brne third_loop
dec r19
brne second_loop
dec r18
brne first_loop
ret
Ok that looks a bit more complex. CPUs don't have a high level delay function. You have to burn cycles (16 million per second for the ATmega328p). There is a better way to do this using timers on the CPU itself and interrupts, but that is for another post.
So you need to burn 16 million CPU cycles to delay 1 second. That doesn't sound too bad? There is an instruction to waste a cycle called nop (no operation). Perfect, so after we turn the LED on we'll just add 16 million lines of nop... then another 16 million lines after we turn the LED off... That isn't going to work. Ok so what about a loop? Loops aren't really a thing at this level, but you can create one using the instructions by doing a rjmp (relative jump) which is basically recursion. Or brne (branch if not equal) which is a conditional jump. which in this case we are doing it the variable is not zero. Ok so lets just loop 16 million times... Wait, this is a 8 bit CPU. 8 bits = 255.
To solve the problem of burning 16 million cycles while 8 bit registers only allowing to store integers as high as 255, I went the route of using multiple loops. For time complexity this is roughly O(n^3). This gets us to roughly delay 1 second or 16 million cycles. One thing to note is that there a multiple instructions that take one or more cycles. So if you simply calculate 60 x 255 x 255 you'll come up way short. Each instruction takes at least 1 cycle.
This is bare metal AVR programming using AVR assembly. This gives an idea of what is actually going on. Flipping actual bits in memory which are mapped to a CPU register to turn on or off pins, burning CPU cycles. I enjoyed digging down this low.
Conclusion
This was my journey of doing a direct comparison of how to program one Arduino UNO R3 development board. I loved seeing how each abstraction was different from the others.
What do you want me cover? I plan on covering timer interrupts or sleep modes next. Let me know what you think.