Blink Analysis: Hardware's "Hello, World!" from Programming Language to Voltage

◷ 30 min read

✏ published 2020-11-05

Jack Leightcap

Contents

- Abstract

- Outline

- Arduino IDE

- Arduino IDE: Analysis and Deploying

- Background: Masks, Setting and Resetting Bits

- Port Registers

- C: Blink

- Systems Software: Compiler

- C: Compiling and Deploying

- Quick Intro to Assembly

- Arduino Uno CPU and Architecture

- Timings in Assembly

- AVR Assembly: Blink

- Systems Software: Assembler

- AVR Assembly: Assembling and Deploying

- Quick Intro to Rust

- Rust: Blink

- Systems Software: Build Systems

- Rust: Building and Deploying

Abstract

This is a transcription of a talk I gave for the Northeastern University Wireless Club. The talk targeted other ECE undergraduates unfamiliar, or new to, embedded systems development. The slides and source can be found here.

Most Arduino tutorials take the IDE as a given. This makes sense for ease of use, but what is really behind those "Verify" and "Upload" buttons? Instead of building complex software on top of an IDE, this talk goes in the opposite direction: how does the simplest possible Arduino program work on a deeper level?

Outline

Languages:

Topics Along the Way:

Arduino IDE

This is such a common starting point it's included as a built-in example.

void setup() {
    pinMode(LED_BUILTIN, OUTPUT);
}

void loop() {
    digitalWrite(LED_BUILTIN, HIGH);
    delay(1000);
    digitalWrite(LED_BUILTIN, LOW);
    delay(1000);
}

Click "verify", then "upload" and you get a blinking light. But if you break this program, where are those cryptic looking errors coming from?

Arduino IDE: Analysis and Deploying

The Arduino "language"

To run:

Background: Masks, Setting and Resetting Bits

If we want to turn an LED on and off, that is exposed to the programmer as a bit.

Masking a bit to 1:

x      | 00001001
mask m | 10000001
-------+----------
x + m  | 10001001

This is often notated in a programming language as:

x = x | 0b10000001
x |= 0b10000001

Masking a bit to 0:

x      | 00001001
mask m | 01111110
-------+----------
x × m  | 00001000

This is often notated in a programming language as:

x = x & 0b01111110l
x &= 0b01111110
x &= ~0b10000001

Port Registers

The Data Direction Registers, written "DDR{port}":

The Data Registers, written "PORT{port}":

There are three distinct ports:

PORTB:

PORTC:

PORTD:

Where is Port A you ask? With the A branch of the MBTA green line of course.

See how "PB", "PC", and "PD" map to the actual pins:

Arduino Uno digital pins figure
Arduino Uno Digial Pins, CC BY-SA

There's the LED on bit 5 of port B!

C: Blink

Using our knowledge of how to set and reset bits, as well as how to write to ports; writing the same program in C:

#include <avr/io.h>
#include <util/delay.h>

int
main(void)
{
    // set the data direction register corresponding to B to be write-only
    DDRB |= _BV(DDB5);

    for(;;) {
        // set pin 5
        PORTB |= _BV(PORTB5);

        _delay_ms(1000);

        // reset pin 5
        PORTB &= ~_BV(PORTB5);

        _delay_ms(1000);
    }
    return 0;
}

Systems Software: Compiler

No longer have those nice buttons — have figure out what the Arduino IDE is doing for us.

Of course, "level" is kind of a vague term with regards to programming languages. "Compiler" in the context of systems software usually means something that takes a program and converts it to assembly or machine code.

For the Arduino specifically, we'll be using two compilers:

C: Compiling and Deploying

# C → ELF
; avr-gcc -Os -DF_CPU=16000000UL -mmcu=atmega328p -o blink blink.c

# ELF → HEX
; avr-objcopy -O ihex -R .eeprom blink blink.hex

# HEX → Arduino
; avrdude -F -V -c arduino -p ATMEGA328P -P /dev/ttyACM0 -b 115200 \
          -U flash:w:blink.hex

A question to the reader: that 16000000 in the compilation step is the clock speed. Why does the compiler need to know this?

Quick Intro to Assembly

Assembly 'Language':

Arduino Uno CPU and Architecture

pinout of ATmega328p IC
ATmega328P Pinout, CC BY-SA

Timings in Assembly

How many clock cycles are there in 1s? For a CISC (Complex Instruction Set Computer), the answer is a big it depends. However, remember that in a RISC (or this one at least, of course oversimplifying); each instruction takes exactly one clock cycle.

            16×10^6 cycles
 1 second × -------------- = 16×10^6 cycles
               1 second

As we'll see, reducing complexity for the assembly implementation is worthwhile. To count that many cycles (functionally equivalent to instructions in this case), we'll use an approximation:

16×10^6 cycles ≈ 256cycles × 256cycles × 244cycles

256 is a good number to count to for something with 8-bit regiters: if you count up to 256, the register can't hold values greater than 255 (2^8 - 1), so 255 + 1 overflows back to 0.

So, we'll count by overflowing two registers (255 → 0), count down from 244 in another for a product of approximately the correct number of cycles.

.equ DDRB, 0x04 // the numeric value of DDRB: there are no human-readable variables in assembly!
.equ PORTB, 0x05 // likewise for PORTB
.org 0x0000 // put this program at the very start of memory

rjmp reset

reset:
    // set port B to be write-only
    ldi R16, 0x20 // 0b00100000
    out DDRB, R16 // DDRB |= 0b00100000

    ldi R20, 0x20 // 0b00100000, the value we'll be toggling the LED with

    // initalize the three counting registers
    ldi R18, 0x00
    ldi R17, 0x00
loop:
    ldi R19, 0xF4 // = 244

    // a loop that keeps counting up, effectively delaying the writes to the LED
delay:
    inc R17
    cpi R17, 0x00
    brne delay // did R17 overflow yet?

    inc R18
    cpi R18, 0x00
    brne delay // did R18 overflow yet?

    inc R19
    cpi R19, 0x00
    brne delay // did R19 overflow yet?

    eor R16, R20 // XOR acts as a "toggle" - toggle the corresponding LED bit
    out PORTB, R16

    rjmp loop // repeat ad infinitum

Systems Software: Assembler

For the Arduino specifically:

AVR Assembly: Assembling and Deploying

# AVR Assembly → Object file
; avr-as blink.asm -o blink.o

# Object file → ELF
; avr-ld -Ttext 0 -nostdlib -nostartfiles  blink.o -o blink.elf

# ELF → HEX
; avr-objcopy -O ihex blink.elf blink.hex

# HEX → Arduino
; avrdude -F -V -c arduino -p ATMEGA328P -P /dev/ttyACM0 -b 115200 \
          -U flash:w:blink.hex

Quick Intro to Rust

Language | First Appeared
---------+-------------------------------------------------------------
Assembly | Ada Lovelace (1843), Kathleen Booth (1947), Atmel AVR (1996)
C        | Dennis Ritchie and Ken Thompson (1972)
C++      | Bjarne Stroustrup (1985), Arduino (2005)
Rust     | Graydon Hoare (2010)

#![feature(llvm_asm)]
#![no_std]
#![no_main]

use ruduino::cores::atmega328 as avr_core;
use ruduino::Register;
use avr_core::{DDRB, PORTB};

fn small_delay() {
    for _ in 0..400000 { unsafe{llvm_asm!("" :::: "volatile")} }
}

#[no_mangle]
pub extern fn main() {
    DDRB::set_mask_raw(0xFFu8);

    loop {
        PORTB::set_mask_raw(0xFF);
        small_delay();
        PORTB::unset_mask_raw(0xFF);
        small_delay();
    }
}

Systems Software: Build Systems

The configuration to build this Rust program:

[package]
name = "blink"
version = "0.1.0"
authors = ["Dylan McKay <me@dylanmckay.io>"]
edition = '2018'
[dependencies]
ruduino = "0.2"
[profile.dev]
panic = "abort"

Rust: Building and Deploying

# Rust → ELF
; cargo build -Z build-std=core --target avr-atmega328p.json

# ELF → HEX
; avr-objcopy -O ihex -R .eeprom blink blink.hex

# ELF → Arduino
; avrdude -D -F -V -c arduino -p ATMEGA328P -P /dev/ttyACM0 -b 115200 \
          -U flash:w:blink.elf:e