Yeah, nah, aye

Main categories:

Running AVR code in QEMU

A quick-start guide to accelerate AVR firmware development

Most of the software I write for embedded systems runs in Linux userspace. It's fairly rare I have to pull out an AVR toolchain, but it does still happen from time to time. I'm normally reluctant to do it, due to:

  1. the relatively slow pace of iteration
  2. the relative difficulty with which software is debugged

For Linux software, I can comfortably compile most things natively on my development workstation, get 80% of the functionality implemented and tested, before doing some final passes to complete that last 20% that is hardware-specific and/or hard to mock. Debugging is also a breeze, either using gdbserver in userspace, or my favourite bug sledgehammer (wrecking ball?), a JTAG adapter.

There are indeed existing simulators out there for AVR microcontrollers, but I was keen to learn that QEMU has gained some support for AVR CPUs as recently as August 2020 (changelog, tag). The power of QEMU over grass-roots simulators/emulators is that there is a consistent interface across different CPUs and platforms, along with an integrated GDB server. This makes it easy to chop and change switching between projects for different platforms.

Virtual Blinky

First things first, let's whip up a "blinky" of sorts. QEMU's emulated AVRs don't have GPIO blocks - the only peripherals they have at the time of writing are timers and a USART controller. This shouldn't be a problem, since you should have been mocking or stubbing your interaction with peripherals out in order to write your unit tests anyway.

Our blinky will instead be toggling a bit in a variable which is mocking the PORTA register. This is a bit of a simplified example, and you're likely not to mock to this low level, but the principle stands.

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

#define LED_PIN (2)

#ifdef MOCK
int mock_porta;
# define LED_PORT mock_porta
#else
# define LED_PORT PORTA
#endif

int main(void)
{
    DDRA |= 1 << LED_PIN;
    while (1) {
        LED_PORT ^= 1 << LED_PIN;
        _delay_ms(1000);
    }
}

This example, when compiled for real hardware (i.e. without -DMOCK) would toggle port A bit 2 at a rate of 500 mHz (millihertz). Let's prove that our XOR toggle logic is correct, without having to program this onto real hardware and wire up an LED.

Saved as blinky.c, let's compile it with MOCK and sufficient debug enabled so that we can resolve preprocessor macros:

$ avr-gcc -gdwarf-2 -g3 -DMOCK -DF_CPU=16000000UL -Os -mmcu=atmega2560 -o blinky.elf blinky.c
$ file blinky.elf
blinky.elf: ELF 32-bit LSB executable, Atmel AVR 8-bit, version 1 (SYSV), statically linked, with debug_info, not stripped

Now launch in QEMU, halting the CPU when it first starts, and listening on TCP port 1234 for gdb remote control.

$ qemu-system-avr -S -s -nographic -machine mega2560 -bios blinky.elf

This should hang, and we can connect AVR GDB to QEMU in another terminal. Note we also point it to the ELF so that it can resolve symbols and other debug information, based on the program counter QEMU reports.

$ avr-gdb -ex 'target remote :1234' blinky.elf

Enter list or l at the GDB prompt a few times until you see the line LED_PORT ^= .... This is the line where we want to set a breakpoint to check our logic works. Note the line number and set a breakpoint there:

>>> l
12
13      int main(void)
14      {
15              DDRA |= 1 << LED_PIN;
16              while (1) {
17                      LED_PORT ^= 1 << LED_PIN;
18                      _delay_ms(1000);
19              }
20      }
>>> b 17
Breakpoint 1 at 0x112: file blinky.c, line 17.

Continue the program execution until the CPU hits that breakpoint with continue or c. Once broken, let's print out the mocked GPIO port register:

>>> p/t LED_PORT
$1 = 0

Before the first iteration of the loop, the port is all zero. This may differ for you if there is junk in the memory location used for the mock port - note that we didn't initialise this memory. The important thing is that our program toggles the value of a bit in this register so let's check for that by continuing program execution until we next hit our breakpoint, in the next iteration of the loop:

>>> c
...
>>> p/t LED_PORT
$2 = 100

We have a bit set! Note that p/t is "print in binary" and bit 2 is set, while it wasn't before. If you like, you could do some bitmath to see only the mocked GPIO line we have the imaginary LED attached to:

>>> p (LED_PORT >> LED_PIN) & 1
$3 = 1
>>> c
>>> p (LED_PORT >> LED_PIN) & 1
$4 = 0
>>> c
>>> p (LED_PORT >> LED_PIN) & 1
$5 = 1

It looks like our first virtual blinky (simublinky? emublinky? qemublinky?) works a treat! With confidence in our algorithm, we could now take this same code, compile without -DMOCK, flash it onto a real AVR and watch the LED blink.

You can kill the QEMU session with k or mon q in GDB.

Closing thoughts

The idea of mocking peripherals such as GPIO, TWI or SPI controllers is probably a little too complex for more complicated projects. Same as with unit testing, you want to be exercising your "business logic" with virtual targets. Larger and more complex microcontroller projects will have less to do with the individual peripherals/controllers and more to do with the algorithms that take data from these peripherals, transform it in some way, and put the output through some other peripheral. Such algorithms are easily tested on virtual platforms with little to no mocking. Develop your core, platform-agnostic logic in virtualised hardware, iterate it, perfect it, and spend only a few passes testing the comparatively simple integration with hardware peripherals.

I initially scoffed at the idea of having no peripherals except timers and USART on a virtual AVR, but the more I thought about it, the more sense it made. Cool as it would be to hook a virtual AVR up to a GTK program of LEDs and switches, there's no real need for this outside perhaps the education of newbies to microcontrollers.

Separating your business logic from peripheral interfacing code should be easy anyway. As mentioned before, you should already have separated these two to the point where it can be quickly stubbed or mocked while you were already writing unit tests for your code.