Links

Ben Laurie blathering


Grown-up Arduino Programming

As I mentioned in a previous post, I am not a big a fan of the Arduino IDE. Since writing that, I’ve discovered I like it even less, because it does some ad-hoc mangling of what you write to turn it from a nearly-C language into genuine C. As a result, it is possible to write some C++ and get away with it, but whether C++ stuff works or not seems entirely random.

It may sound nuts to want to write C++ for a processor so small. But read on – it turns out you can do some nice things at essentially zero cost. But first you need a proper C++ toolchain. One way to get hold of it would be to install the Arduino IDE, in fact, since it uses it under the hood. But I did it from scratch on my FreeBSD system. This turns out to be mostly easy, but there were a couple of wrinkles worth writing down for the greater good.

Firstly, on FreeBSD the compiler is available as a port, so I just installed it with my favourite ports tool

portmaster devel/avr-gcc

(note that there are several variants based on different versions of the compiler available – this is the default variant which, at the time of writing, is based on gcc 4.2.4).

Sadly, although the libc part of the toolchain is available as a port, too, at the time of writing both versions (devel/avr-libc and devel/avr-libc-devel) are broken because they depend on defunct source code. So, I had to build this one by hand, starting with avr-libc-1.6.8.tar.bz2 from http://download.savannah.gnu.org/releases/avr-libc/. This is not too hard, just a slightly customised configuration followed by the usual make commands:

./configure --prefix=/usr/local/avr --host=avr
make
make install

Setting the prefix to /usr/local/avr is advisable as some things get installed immediately below the prefix and so could conflict with native compilers and libraries. However, it does cause some things to end up in /usr/local/avr/avr. Oh, well.

Next up, a test program is a good idea. avr-glibc comes with demo code, which can be found in /usr/local/avr/share/doc/avr-libc-1.6.8/examples/, but none of it is particularly well suited to an Arduino. So, I stole the Makefile from the demo sample and used this code instead of demo.c

#include <avr/interrupt.h>
#include <avr/io.h>

#define FLASH		PB5  // "Pin 13" (Arduino pin) - ATmega168 pin 19
#define CONTROL_PORT	PORTB
#define CONTROL_DDR	DDRB

static void ioinit(void)
    {
    CONTROL_DDR = _BV(FLASH);
    }

int main(void)
    {
    long n;

    ioinit();

    for( ; ; )
	{
	CONTROL_PORT &= ~_BV(FLASH);
	for(n=0; n < 300000; ++n)
	    ;
	CONTROL_PORT |= _BV(FLASH);
	for(n=0; n < 300000; ++n)
	    ;
	}
    }

and modified the Makefile to remove optimisation (essential, otherwise the delay loops get optimised away), select the right CPU (atmega168) and to modify these two lines

DEFS = -I /usr/local/avr/avr/include
LIBS = -B /usr/local/avr/avr/lib

-B is a new flag to me: it specifies where binaries and the crt0 files are found. The last ingredient is a way to upload to the Arduino. The utility avrdude can do this for you

avrdude -p m168 -P /dev/cuaU0 -c arduino -b 19200 -U flash:w:yourstuff.hex

Of course, delay loops are horrible, so my second attempt does this properly, using a timer interrupt. And this is where the C++ comes in: the “standard” way to set up the CPU is to write code like

#define FLASH		PB5  // "Pin 13" (Arduino pin) - ATmega168 pin 19
#define CONTROL_PORT	PORTB
#define CONTROL_DDR	DDRB

static void ioinit(void)
    {
    // PWM, 10-bit, phase-correct
    TCCR1A = _BV(WGM10) | _BV(WGM11);
    // Pre-scaler set to 1024
    TCCR1B = _BV(CS12) | _BV(CS10);
    // Set flash pin to output
    CONTROL_DDR = _BV(FLASH);
    // Enable timer 1 overflow interrupt
    TIMSK1 = _BV(TOIE1);
    sei();
    }

which is pretty revolting and involves a lot of manual-reading to understand. So, as is my habit when dealing with hardware, I tried wrapping it up in nice C++ classes to see what the run-time cost is. I won’t show the C++ classes here as they’re quite verbose and are a work in progress, but the net effect on the setup code is that it now looks like this

#define FLASH		PortB5  // "Pin 13" (Arduino pin) - ATmega168 pin 19

static void ioinit(void)
    {
    Control c;

    c.tc1.OC1ADisconnected();
    c.tc1.OC1BDisconnected();
    c.tc1.PWMPhaseCorrect10Bit();
    c.tc1.Prescaler1024();
    c.tc1.OverflowInterruptEnable();

    FLASH::Out(&c);

    c.Set();

    sei();
    }

which I hope you’ll agree is much more readable. The amazing thing is that, despite the increased verbosity, there’s no cost at all: this produces almost exactly the same assembler as the original code (it is in a slightly different order, though even that could be fixed if needed). The wonders of optimisation.

Note, by the way, the use of a PWM mode is simply because the demo code I borrowed from actually did use PWM – but pin 19 (where the LED is on a standard Ardunio/Freeduino) isn’t a PWM pin, so my code just uses the timer interrupt to time when to turn the LED on or off. The PWM is not really needed but the timer has to be in some mode, so I haven’t yet bothered to figure out a more appropriate one.

When I’ve got more stuff encapsulated in C++ I’ll start sharing the code.

3 Comments

  1. Some random kibitzing:

    avr-libc supplies some calibrated delay loops – see http://www.nongnu.org/avr-libc/user-manual/group__util__delay.html.

    You can set the timer to “normal mode” if you simply want something that counts from 0 to 0xffff, and wrap around again. On reset, timer1 in the ATmega168 defaults to normal mode.

    You probably already know this, but you can simply poll to timer overflow flag (the TOV1 bit in TIFR1) to see when the timer wraps around. You clear the flag by writing a “1” to that flag. This’ll avoid the need for interrupt handling.

    Comment by stephenm — 18 Dec 2010 @ 4:53

  2. Stephen: I confess I have not read enough manual for this to be anything but a guess, but … if I did it by polling wouldn’t I be unable to put the CPU to sleep? That is, the interrupt is needed to wake from sleep mode?

    Comment by Ben — 18 Dec 2010 @ 13:47

  3. If you wanted to put the CPU to sleep, you could have an empty timer overflow interrupt (do something like “EMPTY_INTERRUPT(TIMER1_OVF1_vect);”). And your poll loop would then poll the match flag (OCF1A/OCF1B in TIFR1).

    But doing all of this seems convoluted, and so if you do need to put the CPU to sleep, using an interrupt in the normal way sounds more appropriate.

    Comment by stephenm — 21 Dec 2010 @ 4:40

RSS feed for comments on this post.

Sorry, the comment form is closed at this time.

Powered by WordPress