Arduino Blink — Blackmagic
A CS journey into the black-hole that is Arduino
Writing code is fun, but there’s just something special about building something tangible: It feels more real.
The idea that you can design a system that can act on its own accord fascinated me. See, it’s been almost a decade since I got into Embedded Systems, that’s a little more than a third of my life, and the curiosity hasn’t subsided.
I never went to school for EE, and I think that bolstered my curiosity. I wasn’t weighted down by formulas and equations that I had no context to understand. I wanted to build something, and I’d incrementally learn how to do it.
NERF Sentry
The first project I involved myself in was an automated Nerf Gun (PS: Turn your volume down, there some static noise in the mic).
I used an Arduino microcontroller to build it and hot glued a servo to the trigger (a motor where position can be controlled precisely through code). An infrared (or heat) sensor detected when I walked in front of it and triggered the servo to move, hence shooting me.
Note I knew little about programming and was simply googling phrases like:
- How do I detect a change in the heat sensor on Arduino?
- How do I move a servo in Arduino?
- What are the wires of the servo on the Arduino?
Though I understood very little about how these things worked, I had the confidence to figure it out. Asking these simple questions and building on those answers got me to a working prototype.
The ide
(integrated development environment) I used to code this project is screen-shotted below. There are only two functions Arduino exposes: setup
(which is called once) and loop
(which get’s called in a loop).
But this article isn’t about explaining how to start with an Arduino. Frankly, there are dozens of other more qualified articles on the topic and it would be a waste of my time to redo another tutorial. Here’s a good one:
BREAKDOWN
The Arduino is a microcontroller with many different components on it. Some of the major ones include:
- ATMega82u — Bridge between Computer USB and the main chip on the board. This chip contains firmware so it isn’t intended to be programmed by the user of the board.
- ATMega328pu — The main processing chip on the board, where your programs are run.
- 5V Regulator — Is a low drop-out voltage regulator. Meaning that the input voltage can be as low as 6V and the output is still regulated to 5V
- Crystal oscillator — This serves as the clock or the pulse of the processor. This pulse keeps all of the computations in sync.
This article IS about CS so let’s look at how programs run on the ATMega328pu Chip (The brains of the Arduino Uno).
ARDUINO PROGRAMMING
As we’ve seen above the Arduino ide exposes only two required functions: the setup
and the loop
functions.
This is a bit odd to those who are used to C and C++ programming. Where’s the main function? An initial glance at the code makes us think that this language similar to C, but upon further inspection, we see that the files end with a different file extension:.ino
? We can press the run button but how does that translate to code being compiled? Is there a custom compiler?
To solve this mystery, I dug into the Arduino source code and found something interesting. A main.cpp
file:
In this main.cpp
file, both setup
and loop
were being called. The Arduino language was effectively just C++ code. Now I could really dig into it.
COMPILER
So with any compiled code, there is usually a compiler or a program that helps turn the code into bits the computer, or in this case, the microcontroller can use.
The Arduino IDE was just a pretty wrapper around this compiler. A GUI is limited to what it can expose in a graphical format. I suspected that the Arduino IDE was actually calling into this mystery compiler. There were many more gems to uncover. I knew that my key to understanding them was figuring out this compiler toolchain.
The Arduino Wiki was more than helpful in answering this question.
The Arduino IDE used the avr-gcc
compiler. A simple google search told me how to install this compiler onto my mac:
$ brew tap osx-cross/avr
$ brew install avr-libc
I did a simple tab complete on avr-
to see what other gems were installed on my mac.
Each of those programs serves a specific purpose and to utilize them, let’s take an example use-case.
BLINKING LED
Here’s the Blinking sketch from the Arduino Examples:
It’s a good tutorial for how to create a blinking LED. Here’s my code for it below in the Arduino IDE.
See how the number of bytes that this sketch CLAIMS to use is only 942
. In-fact though the truth is far worse. When you enable verbose in Arduino to see the build commands:
And look at the actual hex
file, the bytes that are uploaded to the microcontroller. You’ll see a far different story. Looking at the build folder the hex
file has a size of 2664 bytes
. This is practically nothing for modern computers, but for certain embedded applications, we’re literally throwing away compute resources. Not good:
Can we do better?
AVR-GCC
First, let’s start with a C implementation. We want to turn a pin on and off to make a light blink.
Turn on the electricity and the light shines, turn off the electricity and the light goes dark.
Accessing pins in an AVR microcontroller is a simple matter of writing to physical registers. To access the memory address of these registers we include this header file:
#include <avr/io.h>
This header file is different for every AVR microprocessor and creates different variables we can access to modify memory. Looking at the datasheet for the ATMega328pu, we see that the register we are interested in activating is the DDRB
register and bit 5
of this register equates to Pin 13 on the Arduino UNO. This register controls the data direction (hence the DD
prefix), and if we set the 5th bit to 1
, we’ll have an output register:
PORTS
To set that bit five, we’ll shift 1
five times then bitwise or
it with the register:
DDRB |= (1 << 5);
By or
-ing the register, if bit five was already set to output then there’s no change, if bit five wasn’t set to output then this bitwise or
set it to output. A bitwise or
is a fairly cheap operation.
Now that we have enabled pin 13, we need to actually set it high
or low
. To do this we have another register called PORTB
and this controls the actual signals coming out of the pin.
To set the pin value to high we simply use the same technique that we used above:
PORTB |= (1 << 5);
This sets the 5th bit of PORTB
to a high signal (hence the light turns on).
Now we need to set the 5th bit of PORTB
to low and leave the remaining bits untouched (to turn off the light). A combination of a not
operator and an and
operator can achieve this:
PORTB &= ~(1 << 5);
Understand what the code above is doing. In every instance it sets the 5th bit of register PORTB
to low while leaving all other bits unchanged.
DELAY
Great, now we need to introduce a delay in-between the blinks just like the original Arduino code. To do so we can import the specific header.
#include <util/delay.h>
To explore all available headers, download the source code here: http://download.savannah.gnu.org/releases/avr-libc/)
And in this header file there’s a function we can use:
void _delay_ms(double __ms)
Calling this function like this will give us our needed 1-second delay:
_delay_ms(1000);
C CODE
The entire C code looks like this all put together:
#include <avr/io.h>
#include <util/delay.h>int main(void) {
DDRB |= (1 << 5);
while(1) {
PORTB |= (1 << 5);
_delay_ms(1000);
PORTB &= ~(1 << 5);
_delay_ms(1000);
}
}
I’ll admit it’s less readable than the Arduino code but let’s see what this overhead gives us.
To compile the code we use these build commands:
$ avr-gcc -Os -DF_CPU=16000000UL -mmcu=atmega328p -c -o main.o main.cpp
$ avr-gcc -mmcu=atmega328p main.o -o main.elf
$ avr-objcopy -O ihex -R .eeprom main.elf main.hex
Let’s take a look at the hex
file size from this compilation.
Boom, it’s only 508 bytes
. That’s an 80% reduction in the size of the build! In an embedded system this is huge. But we’ve made the code un-readable.
READABLE CODE
Let’s refactor the code to functions. Moving out the bitshifts into functions makes the code more readable:
#include <avr/io.h>
#include <util/delay.h>void enablePin13() {
DDRB |= (1 << 5);
}void setPin13High() {
PORTB |= (1 << 5);
}void setPin13Low() {
PORTB &= ~(1 << 5);
}int main(void) {
enablePin13();
while(1) {
setPin13High();
_delay_ms(1000);
setPin13Low();
_delay_ms(1000);
}
}
But recompiling we see that the size of the code increased a bit:
Why?
Because function calls have an overhead associated with them and we have to be very cognizant of it. If we inline
these functions the compilation size goes back down. Zero-Cost
abstractions!
#include <avr/io.h>
#include <util/delay.h>inline void enablePin13() {
DDRB |= (1 << 5);
}inline void setPin13High() {
PORTB |= (1 << 5);
}inline void setPin13Low() {
PORTB &= ~(1 << 5);
}int main(void) {
enablePin13();
while(1) {
setPin13High();
_delay_ms(1000);
setPin13Low();
_delay_ms(1000);
}
}
And the compilation size is back down to 508
:
C++
What if we wrapped the blinking LED into a class in C++?
class LED {
public:
LED() {
DDRB |= (1 << 5);
} ~LED() {} void blink() {
while(1) {
this->on();
_delay_ms(1000);
this->off();
_delay_ms(1000);
}
}
private:
void on() {
PORTB |= (1 << 5);
} void off() {
PORTB &= ~(1 << 5);
}
};
Looks fairly straightforward, I can call it like this in the main
function:
int main(void) {
LED l;
l.blink();
}
It really simplifies the readability, but how are we doing on program size?
Again 508
bytes! Not bad. Again a Zero-Cost Abstraction
. So what happens when we split the class up into different files. We’ll create a header file that contains the class LED
and a cpp
file that has the class definition.
led.hpp:
#ifndef LED_H
#define LED_Hnamespace light {class LED {
public:
LED();
~LED();void blink() const;
private:
void on() const;
void off() const;
};} // namespace light#endif
led.cpp
#include "led.hpp"#include <avr/io.h>
#include <util/delay.h>namespace light {
LED::LED() {
DDRB |= (1 << 5);
}LED::~LED() {}void LED::blink() const {
while(1) {
this->on();
_delay_ms(1000);
this->off();
_delay_ms(1000);
}
}void LED::on() const {
PORTB |= (1 << 5);
}void LED::off() const {
PORTB &= ~(1 << 5);
}} // namespace light
main.cpp
#include "led.hpp"using namespace light;int main(void) {
LED l;
l.blink();
}
compiler commands
$ avr-g++ -Os -DF_CPU=16000000UL -mmcu=atmega328p -c -o main.o main.cpp
$ avr-g++ -Os -DF_CPU=16000000UL -mmcu=atmega328p -c -o led.o led.cpp
$ avr-g++ -mmcu=atmega328p main.o led.o -o main.elf
$ avr-objcopy -O ihex -R .eeprom main.elf main.hex
Alright let’s look at the size of main.hex
:
680
bytes, oh the blasphemy! What’s going on?
The compiler isn’t able to inline
the functions we’ve defined since they’re in a different cpp
file from main.
A little trick we can use is to simply inline the functions in the hpp
file.
By removing the led.cpp
file entirely and defining the class in led.hpp
we now have another Zero-Cost Abstraction
.
new led.hpp
#ifndef LED_H
#define LED_H#include <avr/io.h>
#include <util/delay.h>namespace light {class LED {
public:
LED() {
DDRB |= (1 << 5);
}
~LED() {}
void blink() {
while(1) {
this->on();
_delay_ms(1000);
this->off();
_delay_ms(1000);
}
}
private:
void on() {
PORTB |= (1 << 5);
}
void off() {
PORTB &= ~(1 << 5);
}
};} // namespace light#endif
new compiler commands
$ avr-g++ -Os -DF_CPU=16000000UL -mmcu=atmega328p -c -o main.o main.cpp
$ avr-g++ -mmcu=atmega328p main.o -o main.elf
$ avr-objcopy -O ihex -R .eeprom main.elf main.hex
What’s the size now? Back to our comfortable 508
bytes.
Plus we’ve made the C code arguably more understandable with C++ abstractions.
Arbitrary Pins (Cherry on Top)
The compiler is really, really smart if you let it do its job. Defining an arbitrary -pin as an input to the class and introducing an array of pin names (defined in avr/io.h
) did nothing to increase the size of the compiled hex file. Here’s an example:
main.cpp
#include <avr/io.h>
#include <util/delay.h>#include "led.hpp"using namespace light;namespace {constexpr int SELECTED_PIN = 5;constexpr const int pin_array[] = {DDB0, DDB1, DDB2, DDB3, DDB4, DDB5};}; // namespaceint main(void) {
int pin = pin_array[SELECTED_PIN];
LED l(pin);
l.blink();
}
led.hpp
#ifndef LED_H
#define LED_H#include <avr/io.h>
#include <util/delay.h>namespace light {class LED {
private:
int pin_;public:
LED(int pin) : pin_(pin) { DDRB |= (1 << pin_); }
~LED() {}
void blink() {
while (1) {
this->on();
_delay_ms(1000);
this->off();
_delay_ms(1000);
}
}private:
void on() { PORTB |= (1 << pin_); }
void off() { PORTB &= ~(1 << pin_); }
};} // namespace light#endif
compiler commands
$ avr-g++ -Os -DF_CPU=16000000UL -mmcu=atmega328p -c -o main.o main.cpp
$ avr-g++ -mmcu=atmega328p main.o -o main.elf
$ avr-objcopy -O ihex -R .eeprom main.elf main.hex
And the resulting hex
file size:
Template Class
Well how about a templated class version of Led
? Seems fun, let’s see the size of the built hex
file with that class.
led.hpp
#ifndef LED_H
#define LED_H#include <avr/io.h>
#include <util/delay.h>namespace light {template <unsigned int PIN>
class LED {
public:
LED() { DDRB |= (1 << PIN); }
~LED() {}
void blink() {
while (1) {
this->on();
_delay_ms(1000);
this->off();
_delay_ms(1000);
}
}private:
void on() { PORTB |= (1 << PIN); }
void off() { PORTB &= ~(1 << PIN); }
};} // namespace light#endif
main.cpp
#include <avr/io.h>
#include <util/delay.h>
#include "led.hpp"using namespace light;namespace {constexpr int SELECTED_PIN = 5;constexpr const int pin_array[] = {DDB0, DDB1, DDB2, DDB3, DDB4, DDB5};}; // namespaceint main(void) {
const int selected_pin = pin_array[SELECTED_PIN];
LED<selected_pin> l;
l.blink();
}
compiler commands
$ avr-g++ -Os -DF_CPU=16000000UL -mmcu=atmega328p -c -o main.o main.cpp
$ avr-g++ -mmcu=atmega328p main.o -o main.elf
$ avr-objcopy -O ihex -R .eeprom main.elf main.hex
And the resulting size is:
I think we’ve beaten a dead horse by now. C++ can make your Embedded Code more readable without adding to the overhead of the built file size.
Follow me on twitter here: https://twitter.com/AbhishekPratapa
Comment down below! I don’t claim to be an expert on these things but I love writing about it. Curious to see what others are working on as well!