Mau Archive Links Publications GitHub

09 May 2018
6 min read

Bare Metal

A common practice in software engineering is to avoid repetition and unnecessary complexity by encapsulating instructions into routines. Collections of such routines become the libraries that make certain programming languages better suited to tackle specific problems. A well know environment with such libraries for microcontroller programming is the Arduino platform, which abstracts away most of the initial and time-consuming setup step.

However, such easy-to-use approach may cost a lot in the long run, with slow or undocumented routines and changes from such libraries breaking projects, leaving novice users without a clue. Without the need for a complete understanding of what is happening under the hood more people can play with electronics, but few can actually build a long-term project out of it. In the following sections we explore the old blocks of microcontroller programming that are sometimes ignored by tutorials around the web.

Selecting a microcontroller

Instead of searching for the best microcontroller ever, one must identify which features are important to each project. Such features may include:

It is a good idea for starters to pick the most available microcontroller, due to their low-cost and lots of materials available online. Such materials will minimize development time, with libraries ported and tested in the selected platform. Only pick a different microcontroller when the specification demands, for example, hardware division must be available for performance reasons or energy consumption must be small to improve battery life.

For complex projects the memory size and amount of GPIOs must be carefully considered, as well as their placement around the IC. Remember that some microcontrollers are 5V tolerant, which can save a few extra components. The tools are usually free, their real cost is their learning curve, which may consume a long time for inexperienced users. To avoid being tied to one IDE it is a good idea to start with a Makefile, which reveals the development stages instead of hiding the entire process behind a few buttons.

Here we will focus on the Arduino/AVR microcontrollers due to their low-cost, availability and vast library support.

Makefile

To compile and flash a project outside an IDE one needs to execute separate tools in a specific order. These tedious command sequences can be accomplished by a Makefile. The Makefile can be used with make or make all to compile the project, make flash to program the microcontroller and make clean to remove generated files. The following is my Makefile, based on Florent Flament’s post.

TARGET = main
BAUD = 57600
AVR  = atmega328p
TYPE = arduino
FREQ = 16000000
PROGRAMMER = /dev/ttyUSB0

CFLAGS  = -DF_CPU=$(FREQ) -mmcu=$(AVR) -Wall -Werror -Wextra -Os
OBJECTS = $(patsubst %.c,%.o,$(wildcard *.c))

.PHONY: flash clean

all: $(TARGET).hex

%.o: %.c
	avr-gcc -c $< -o $@ $(CFLAGS)

$(TARGET).elf: $(OBJECTS)
	avr-gcc -o $@ $^ $(CFLAGS)

$(TARGET).hex: $(TARGET).elf
	avr-objcopy -j .text -j .data -O ihex $^ $@

flash: $(TARGET).hex
	avrdude -p $(AVR) -c $(TYPE) -P $(PROGRAMMER) -b $(BAUD) -v -U flash:w:$<

clean:
	rm -f $(TARGET).hex $(TARGET).elf $(OBJECTS)

Sketches

Common embedded projects usually follow the idea of Wiring sketches, a program with setup and loop functions. During setup everything is initialized once, while the loop is repeatedly executed. These two functions cover most basic programs, complex programs require their own functions and Interrupt Service Routines (ISRs).

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

void loop(void)
{
  digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); // Toggle LED
  delay(1000); // Block program for 1000ms to see LED
}

The entry point of this program, a main function, can be ommitted. Advanced users can add a custom main function to their program and avoid the unnecessary initialization and automatic event handling. The hidden main function of a sketch looks like the following:

int main(void)
{
  init(); // Configures internal peripherals, such as timer used by delay
  setup();
  while(true)
  {
    loop();
    if(event_available) execute_event(); // Execute external events
  }
}

Pins

The most common usage of a microcontroller is to observe and control the state of pins. The pins are exposed through port registers:

Arduino abstracts pin numbering with pinMode and digitalRead/Write, but the problem lies in the implementation. A look-up table is used to resolve pins at run-time, with a few extra checks to disable PWM and invalid pins, which consumes processing time and memory. Variations of such functions can identify port and pin at compile-time to achieve the same result faster. This is specially useful with protocols that transmit a lot of data, such as the ones in displays, or responsive sensors for hazard applications.

Arduino

void setup(void)
{
  pinMode(2, INPUT);
  pinMode(3, OUTPUT);
}

void loop(void)
{
  char v = digitalRead(2);
  digitalWrite(3, HIGH);
  digitalWrite(3, LOW);
}

C

void setup(void)
{
  DDRD &= ~(1 << PD2);
  DDRD |= 1 << PD3;
}

void loop(void)
{
  char v = (PIND >> PD2) & 1;
  PORTD |= 1 << PD3;
  PORTD &= ~(1 << PD3);
}

Another detail about Arduino is that pinMode usually changes more than DDRx:

pinMode DDRx PORTx
INPUT 0 0
INPUT_PULLUP 0 1
OUTPUT 1 unchanged

This behaviour may not be valid to certain boards, as different vendors may support Arduino functions in their hardware while supplying incompatible implementations of pinMode. Even if the implementation is equal there is a chance only the OUTPUT pins are set, as Arduino starts all pins as INPUTs, something to keep in mind while porting projects.

Fast toggle

If you wanted to change the state of a pin, usually twice, to send a rising or falling edge trigger signal you would probably use digitalRead and digitalWrite. However, if you are certain about the state of such pin, you could skip digitalRead and use two calls to digitalWrite. Such calls to digitalWrite can be replaced by PORTx operations. but they can also be replaced by PINx. Faster toggling can be performed by writing a 1 to the bit in the PINx register instead of reading and writing the PORTx bit. Note that PINx = 1 << Px generates a ldi out pair of instructions, loading and writing a register, while PINx |= 1 << Px generates a single sbi to set a bit in a register. Both options consume 2 clock cycles, but the ldi can happen before a loop, trading an extra instruction (space) for a cycle (time). Applying this knowledge you can avoid the double call to delay, just toggle one per loop and use the smaller _delay_ms. The difference here is over 900 bytes for Arduino to 156 bytes in C, with a much faster code to bit-bang.

Arduino

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

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

C

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

int main(void)
{
  DDRB |= 1 << PB5;
loop:
  PINB |= 1 << PB5;
  _delay_ms(1000);
  goto loop;
}

Barrel shifter

Little microcontrollers may lack not only hardware division, but the useful barrel shifter. Instead they can only shift by one, which means a loop to achieve (n << k) for k > 1. An idea to improve performance without using a better compiler is to avoid shifts altogether, using equivalent instructions.

To test the leftmost bit avoid if(n >> 7), use the equivalent if(n & 0x80). Such single bit tests are actually available as instructions. When in need of the upper 4 bits it is also possible to avoid a shift loop with the swap instruction, which flips the nibbles as in (n << 4) | (n >> 4), and a 0xF mask.

Divide or multiply by 256 a 16 bit integer is free, as the result is already in a byte. For example, to avoid division by 10 one can multiply by 26 and divide by 256, which works for small integers and may be applicable to certain projects, as it was in Aorist. This results in a single multiplication instruction, ignoring the least significant byte. Remember to limit shifts to 1, 4 and 8 bits to take advantage of the available instructions.