Low Level Arduino Programming — LED Blink (Part 1)
In this article, we will blink an LED with the Arduino Uno without using the Arduino IDE or any libraries.
Arduino offers a gentle introduction to the world of microcontrollers and embedded programming. The Arduino IDE includes many features which make it easy to get started: libraries for controlling GPIO pins and communicating over serial, and one-click programming, to name a couple. Whilst this is great for a beginner, it hides a lot of the low level details of how the microcontroller works.
The Arduino Uno contains an Atmel ATMega328p microcontroller. We will rely heavily on the datasheet whilst writing our code. This can be found at https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-7810-Automotive-Microcontrollers-ATmega328P_Datasheet.pdf.
Required Tools
To get started, we need to install the AVR toolchain for building applications, and avrdude for installing them to the Arduino.
Using Debian 10 Linux, this can be done by running:
sudo apt install gcc-avr avrdude
We will also need a text editor, and our application will be written in C.
Registers
Low level microcontroller program involves reading from and writing to registers. A register is a single byte variable, which when accessed causes the microcontroller to perform a predefined action. Individual bits in each register are used for different functions. As an example, the following image from the datasheet describes the PORTB register (we will be using this shortly):
Each bit in this register corresponds to an I/O pin.
Modifying Registers
Setting a bit to 1:
PORTB |= (1 << PORTB5);
Setting multiple bits to 1:
PORTB |= (1 << PORTB5) | (1 << PORTB2);
Setting a bit to 0:
PORTB &= ~(1 << PORTB5);
Setting multiple bits to 0:
PORTB &= ~((1 << PORTB5) | (1 << PORTB2));
A useful AVR library macro can be used alternatively in all of these assignments:
PORTB |= _BV(PORTB5); // _BV(x) is the same as (1 << x)
Controlling I/O Pins
Pin Layout
There are three banks of I/O ports on the ATMega328p. These are described using the letters B, C and D. The following image labels each pin on the Arduino Uno. Each pin has multiple names as many pins have multiple uses.
The pink and pink/green boxes contain the pin numbers as used in the Arduino IDE. The Arduino Uno has a built in LED connected to pin 13. By referencing the black/yellow box on the diagram, we can see that this is actually physical pin PB5 (it is pin number 5 in bank B).
The I/O Registers
The I/O pins are arranged in three banks, labelled B, C and D. Each bank has three registers associated with it for controlling the pins on that bank. Each bit in the register controls a single pin. Since we will be working with bank B, all following examples will use it. Banks C and D work identically.
DDRB
The DDRB register is used to set the direction of a pin. Writing a 1 to the relevant bit will set the pin to an output and writing 0 will set it to an input.
PORTB
The PORTB register has two uses, depending on whether the pin being controlled is an input or an output.
If the pin is an input, writing a 1 to the relevant bit enables the built-in pull-up resistor on that pin, and writing a 0 disables it.
It the pin is an output, writing a 1 to the relevant bit will drive the output high, and writing a 0 will drive it low.
PINB
Writing 1 to a bit in the PINB register toggles the corresponding bit in the PORTB register.
The PINB can also be used to read the level on a pin when set as either an input or an output.
Bringing It All Together
Let’s bring everything together into a file: blink.c.
To use these registers in our C code, we must first include the AVR header file which contains them. We will also include the header which defines the delay function we can use to sleep:
#include <avr/io.h>
#include <util/delay.h>
Next, we can write our main function. We will initialise the LED pin as an output, and then toggle it in an infinite loop:
int main(void)
{
DDRB |= _BV(DDB5); // Set LED as an output while (1)
{
PORTB |= _BV(PORTB5);
_delay_ms(500);
PORTB &= ~_BV(PORTB5);
_delay_ms(500);
} return 0; // We will never get here!
}
Alternatively, we could replace our infinite loop with:
while (1)
{
PINB |= _BV(PINB5);
_delay_ms(500);
}
Building and Programming
First we must build the binary:
avr-gcc -Os -DF_CPU=16000000 -mmcu=atmega328p -o blink.elf blink.c
We must define the CPU frequency (16MHz) so the delay function knows exactly how many clock ticks 1 millisecond is.
Next we extract the raw hex from the ELF binary:
avr-objcopy -O ihex blink.elf blink.hex
And finally we can burn it to the Arduino!
avrdude -p atmega328p -c arduino -P /dev/ttyACM0 -b 115200 -U flash:w:blink.hex
Now enjoy all your hard work in the form of a flashing LED!