Running D on a microcontroller

The LLVM project recently merged an experimental AVR back-end, and as it turns out, this back-end is usable from the D programming language using the LDC compiler!

AVR is an instruction set interpreted by some Harvard architecture microcontrollers. When you compile your D program to AVR and run it on a suitable microcontroller, it will start executing the main function as usual. The program can interact with the outside world through I/O pins and interrupts.

In this series of blog posts I will be making a quartz watch with torch and radio clock synchronization. This is the first post in this series.

Programming for AVR

There exist a lot of tutorials on the web about programming for AVR. They give a good overview of all the steps you need to take in order to initialize and use a particular feature, but they typically do not explain what every line of code means in detail, and to properly understand how it works—nobody in their right mind would commit code they didn’t understand—you need to delve into the datasheet. Datasheet is just a fancy word for the manual of an electronic component, such as the ATmega328P microcontroller that I will be using for this project. The datasheet lists the purpose of each bit in each special register, and explains how their settings cooperate to provide a particular feature. It also describes the purpose of each pin. The pins are the metal bits sticking out of the chip.

Photograph of the ATmega328P microcontroller in a DIP-28 chip.
The ATmega328P microcontroller in a DIP-28 chip.

Configuring the timer

In every watch, the most important feature is keeping track of the time. Unlike desktop computers, the ATmega328P does not contain an internal real-time clock, so we will have to implement time-keeping ourselves. There is no magical way to ask the universe what the current time is; it can only be done by counting at a known frequency. Busy looping is the obvious approach, taking advantage of our knowledge of the configurable clock frequency of the ATmega328P, in my case 1 MHz. However, this is very power-inefficient, does not work well at all with interrupts, and is subject to the rather high tolerance of 10% in the internal oscillator.

A better approach is to connect a specific quartz crystal to the ATmega328P. When exciting a crystal with an electric current, it will oscillate at its resonant frequency. This is such a common use case that the ATmega328P has special circuitry that fires an interrupt whenever the crystal has reached a certain number of oscillations. The crystal I use has a resonant frequency of 32 KiHz. This is convenient for two reasons: it is above the maximum acoustic frequency of 20 kHz, so it makes no noise; and it is a power of two, making it easy to divide by another power of two. For more information on the use of and mechanics behind crystals of this particular frequency, see the excellent video on the topic by Steve Mould.

The datasheet contains a nice schematic that tells you how to connect the crystal to the ATmega328P. A schematic displays the topology of an electronic circuit: which components must be connected to each other. How you lay them out geometrically is up to you, although in more complex circuits you must consider the effects of the temperature and electromagnetic fields of nearby components.

Schematic that shows how to connect a crystal to the ATmega328P, on pins XTAL1 and XTAL2, with two equivalent capacitors both connected to GND.
Schematic that shows how to connect a crystal to the ATmega328P.

The datasheet also explains how to set up the timer circuitry to fire interrupts at regular intervals, where an interval is defined as a specific number of oscillations of the crystal. And while we are waiting for the timer to count oscillations, we can put the CPU of the ATmega328P to sleep and significantly reduce power usage. The D source code for configuring the timer is as follows:

// Configure the timer to use the external crystal.
// The external crystal is much more accurate
// compared to the internal oscillator.
ASSR.volatileModify!(a => a.set!AS2);

// The crystal causes a counter to be incremented.
// Set the counter to zero to begin with.
TCNT2.volatileStore(0);

// Enable the timer overflow interrupt.
// This will fire whenever TCNT2 reaches 256.
TIMSK2.volatileModify!(a => a.set!TOIE2);

// Count every 128 pulses, incrementing TCNT2.
// Once the accumulator overflows (i.e. reaches 256),
// the timer overflow interrupt TIMER2_OVF is fired.
// 128 Hz * 256 = 32768 Hz, which matches our crystal.
//
// +------+------+------+------------------+
// | CS02 | CS01 | CS00 | Description      |
// +------+------+------+------------------+
// |    0 |    0 |    0 | No clock source  |
// |    0 |    0 |    1 | Clock I/O        |
// |    0 |    1 |    0 | Clock I/O ÷ 8    |
// |    0 |    1 |    1 | Clock I/O ÷ 32   |
// |    1 |    0 |    0 | Clock I/O ÷ 64   |
// |    1 |    0 |    1 | Clock I/O ÷ 128  |
// |    1 |    1 |    0 | Clock I/O ÷ 256  |
// |    1 |    1 |    1 | Clock I/O ÷ 1024 |
// +------+------+------+------------------+
TCCR2B.volatileModify!(
    a => a
    .set!CS22
    .clr!CS21
    .set!CS20
);

There are three points worthy of mention:

  1. The code is heavily commented, quoting tables from the datasheet where necessary. The datasheet is a PDF, so it is intrinsically annoying to work with, and there is no way you are going to remember all these obscure names and combinations of bits. Comments are a life saver in this scenario.
  2. There are various obscure names in this code, such as AS2 and TCCR2B. These correspond to bit offsets and memory addresses, which are defined in the datasheet and can be defined in D as enums. The ATmega328P exhibits special behavior when memory at these memory-mapped I/O addresses is modified, as opposed to modifications at general-purpose random-access memory addresses which are used for working storage.
  3. The code uses volatile operations to access special registers. When using normal loads and stores (with the * dereferencing operator and the = assignment operator), the compiler will try to optimize away loads and stores that it considers redundant. However, the ATmega328P interprets loads and stores on these registers as special, so we must ensure that the compiler does not optimize them away. By using volatile operations, they will not be removed. The convenient function volatileModify comes from my own module, and does a volatile load followed by a volatile store, after modifying the value with the given function. That module also contains the convenient functions set and clr for setting and clearing particular bits.

After setting up the timer, we can enable interrupts and start waiting for them to fire:

// Enable interrupts and enter low-power mode.
// The chip will wake up whenever an interrupt is fired.
sei();
for (;;) sleep();

Handling AVR interrupts

When an interrupt fires, the corresponding interrupt service routine is called. An interrupt service routine is just a function that has a specific name and calling convention. You can compare them to signal handlers in Unix: they interrupt the program, and the program will continue when they return. LDC currently lacks the necessary attributes to define interrupt service routines. One solution is to define them in C instead, and just have them call a D function:

#include <avr/interrupt.h>

// It is currently not possible to define ISRs in D.
// We define them in C, and delegate to D from here.

#define FORWARD_ISR(name)   \
    void name ## _vector(); \
    ISR(name ## _vect) { name ## _vector(); }

FORWARD_ISR(TIMER2_OVF);

We can then write a simple interrupt service routine that toggles the output of the PB0 pin. This pin can for example be connected to an LED, to toggle it on and off at 1 Hz. We will get to the hardware later!

/**
 * When the chip is powered on,
 * this is where it starts executing.
 */
extern (C) nothrow @nogc @safe
void main()
{
    // Configure I/O ports.
    // Each port consists of multiple pins.
    // A pin can be either in read mode (0)
    // or in write mode (1).
    DDRB.volatileStore(0b00000001);

    /+ set up timer here +/
    /+ wait for interrupts here +/
}

/**
 * Called once a second by the timer.
 * The timer is set up in main.
 */
extern (C) nothrow @nogc @safe
void TIMER2_OVF_vector()
{
    PORTB.volatileModify!(a => cast(ubyte) (a ^ 1));
}

Compiling for AVR

To compile for AVR, we must first install an LLVM version that can target AVR. LLVM ships with an AVR target, but it is not enabled by default. You can enable it by rebuilding LLVM according to the instructions on the D wiki. Then it is just a matter of invoking the compiler:

$ avr-gcc -O3 -mmcu=atmega328p -c interrupt.c
$ ldc2 -betterC -boundscheck=off -dip1000 -O3   \
       -mtriple=avr-atmel-none -mcpu=atmega328p \
       -gcc=avr-gcc -Xcc=-mmcu=atmega328p       \
       -of=watch-firmware.elf interrupt.o *.d

This produces an ELF binary with AVR machine code. The -betterC flag disables some D features that are not available on AVR, such as the garbage collector. The -boundscheck=off flag disables bounds checks when indexing into arrays, as there is no way to deal with assertion errors at the moment.

Every cross-compilation target of GCC requires its own build of the compiler, hence we invoke avr-gcc instead of gcc to compile the C source file defining the interrupt service routines. LLVM supports cross-compilation out of the box, so there is no “avr-ldc2”. Instead, we specify the compilation target with the -mtriple argument.

There are some limitations to how you can use D at the moment. Most of the standard library will not compile due to missing support for AVR in Phobos. With version (AVR) merged, it is now possible to start adding support where possible. Arithmetic using double is not supported, and there are some constructs that cause LDC to crash. But all in all my experience has been positive so far.

Deploying to the ATmega328P

To deploy the built code, I first strip all the ELF overhead. The ATmega328P does not know about files, or even run an operating system. It just runs the code you put on it starting at the beginning.

$ avr-objcopy                \
      --only-section .text   \
      --only-section .data   \
      --output-target binary \
      --pad-to 32768         \
      watch-firmware.elf     \
      watch-firmware.bin

Then I upload the binary to the ATmega328P using the TL866II Plus universal programmer. This device connects to my workstation over USB and can program various chips, including EEPROMs and microcontrollers. There are many other programmers for AVR-based microcontrollers. To use those you’ll need the AVRDUDE program instead of the minipro program.

Photograph of the TL866II Plus universal programmer connected to a laptop, with an ATmega328P microcontroller in a DIP-28 chip inserted into it, ready for programming.
TL866II Plus universal programmer with a correctly inserted ATmega328P, ready for programming.

The TL866II Plus ships with a proprietary program that only runs on Windows. I don’t use Windows and I’m not going to install random software from a Chinese manufacturer, but luckily there is a free software alternative that runs on GNU/Linux: minipro.

$ minipro -p ATMEGA328P@DIP28 \
          -w watch-firmware.bin
Found TL866II+ 04.2.86 (0x256)
  Expected  04.2.111 (0x26f)
  Found     04.2.86 (0x256)
Chip ID OK: 0x1E950F
Erasing... 0.04Sec OK
Writing Code...  2.36Sec  OK
Reading Code...  0.68Sec  OK
Verification OK

Connecting other components

To set up the hardware I use a breadboard and some pre-cut wires, along with the following components:

  • One 100 Ω metal film resistor.
  • One 10 kΩ metal film resistor.
  • Two 15 pF ceramic capacitors.
  • One orange LED.
  • One 32 KiHz quartz crystal.
  • One ATmega328P microcontroller in a DIP-28 chip.
  • Two 1.5 V AA batteries in a battery holder.

The 100 Ω resistor limits the current through the LED. As LEDs don’t limit current themselves, when when a high voltage is applied to them they will be destroyed unless a proper resistor is connected to them in series. The PB0 pin of the ATmega328P is connected to the negative terminal of the battery through the LED and this resistor.

The 10 kΩ resistor connects the RESET pin of the ATmega328P to the positive terminal of the battery. I don’t know for sure if a resistor is needed here or whether a wire is sufficient, but this is what everybody seems to be doing. The reset pin must be connected, otherwise the ATmega328P might randomly reset itself due to there not being a reference potential, causing a floating voltage.

The 15 pF ceramic capacitors are connected between the two pins of the crystal and to the negative terminal of the battery, as instructed by the datasheet of the ATmega328P. The reason these capacitors are necessary is quite technical, so I will not go into that in this post. The exact capacitance needed depends on the geometric layout of the circuit, and will require calibration when deploying to a real device. For this example, 15 pF is Good Enough™.

Finally there are some wires connected to the VCC, AVCC, and GND pins of the ATmega328P and to the respective terminals of the battery, to provide it the power it needs to operate.

Power it on, et voilà!

Video of a breadboard set-up with a ATmega328P microcontroller programmed in D toggling an LED at 1 Hz.
ATmega328P microcontroller programmed in D toggling an LED at 1 Hz.