How Arduino digitalWrite Works – and why AVR is Faster

How Arduino Simplifies Digital I/O – and why AVR is Faster

Inside the Arduino

Have you ever wondered exactly what happens under the hood when you execute a digitalWrite() call on your Arduino UNO? This post gives you a quick look behind the scenes, and hints at why working directly with the bare-metal ATmega328 will be faster.

There’s a lot of talk about the overhead that the Arduino IDE adds to its projects in order to make the lives of the users easier – and the impact that the overhead can have on your projects. As you probably know, I ditched my Arduino UNO a number of years ago, choosing rather to work directly with the microcontroller – not in order to reduce the overhead, but for a number of other reasons that I’ll get into another time. Now that I’m happily working directly with the ATmega328, I thought it would be interesting to look behind the scenes and understand how the simplest of instructions – digitalWrite() – actually works.

Wrapping the AVR up

Essentially, all the Arduino UNO does is to wrap the ATMega328P up into a prettier package for you to use. Underneath the wrapping paper sits the same set of registers, the same basic program structures, and the same compilers. Not only does the wrapper provide a set of functions (eg. DigitalWrite()), but it also provides a slightly different program structure. All of this is implemented in a set of “include” files that are hidden away in the Arduino folder structures. The structure is fairly convoluted, but you’ll find the main include files at …/Arduino/hardware/arduino/avr/cores/arduino

The Program Structure

Before we get onto the digitalWrite() function, let’s understand how the IDE hides all these wrapper functions away from you. When you compile a sketch, the IDE adds some extra code to your sketch before it actually does the compiling – among others it includes the arduino.h file and adds a main() function. The result of this is that the uniquely Arduino sketch (with a setup() and a loop()) is now turned into a “normal” C++ program. Take a look at the main.cpp file in the folder above, and you’ll see that it has the following:

int main(void)
{
    init();
    initVariant();
    #if defined(USBCON)
        USBDevice.attach();
    #endif
    setup();

    for (;;) {
        loop();
        if (serialEventRun) serialEventRun();
    }

    return 0;
}

This should look familiar to you – the setup() and loop() functions you created in a sketch are called from within the main() function, and their function prototypes are declared in the arduino.h file.

The Files that the IDE Includes

The “arduino.h” Include File

The arduino.h file drives the core of the Arduino IDE – it includes the definitions for such keywords such as “HIGH” and “LOW”, some macros (such as the min() and max() functions) as well as the function prototypes for many of the key built-in Arduino functions – including digitalWrite() and digitalRead(). So we’re getting slightly closer to our goal of seeing how the digital I/O is implemented behind the scenes. There’s another #include in this file that we’re interested in: #include "pins_arduino.h". We’ll look at this file next.

The “pins_arduino.h” Include File

This file contains the mapping between the Arduino pins and the underlying microcontroller’s actual pins. As you know, microcontrollers’ pins are usually grouped into ports and pins. The ATmega328, for example, has 3 ports: PB, PC and PD. Within these ports, the individual pins are numbered from 0 to 7. The Arduino IDE hides this by simply referring to pins by whether they are digital (D) or analog(A), with a sequence of numbers within those categories. If you look in the pins_arduino.h file, you’ll note that there’s a useful diagram setting out the pin mappings:

Arduino Pin Mapping
The mapping inside this file takes into account both the port (see the digital_pin_to_port_PGM definition) and the pin numbers (see the digital_pin_to_bit_mask_PGM definition). Another step closer: we now know how the pins are mapped.

The “wiring_digital.c” Source File

This file is where we hit gold – it is here that the digitalWrite() and digitalRead() functions are defined. In order to be generic enough to handle the possible states that the MCU could be in, the digitalWrite() function needs to:

  • Map the Arduino PWM pin to the timer that controls the PWM
  • Map the Arduino pin to the microcontroller’s port
  • Map the Arduino pin to the microcontroller’s pin
  • Turn the PWM off if it is active on the specified pin
  • Disable global interrupts in case they are active
  • Use an IF statement to set the the pin either High or Low

Here’s what that actually looks like:

void digitalWrite(uint8_t pin, uint8_t val)
{
    uint8_t timer = digitalPinToTimer(pin);
    uint8_t bit = digitalPinToBitMask(pin);
    uint8_t port = digitalPinToPort(pin);
    volatile uint8_t *out;

    if (port == NOT_A_PIN) return;

    // If the pin that support PWM output, we need to turn it off
    // before doing a digital write.
    if (timer != NOT_ON_TIMER) turnOffPWM(timer);

    out = portOutputRegister(port);

    uint8_t oldSREG = SREG;
    cli();

    if (val == LOW) {
        *out &= ~bit;
    } else {
        *out |= bit;
    } 

    SREG = oldSREG;
}

What Does This all Mean?

Essentially, by digging into the underlying Arduino libraries, we’ve proved what common sense has probably been telling us all along – that there’s no such thing as a free lunch. This applies the world over, and not only in the sphere of electronics – but it is perhaps easier to measure in our field. We’d naturally expect there to be trade-offs between ease-of-use and optimisation/efficiency; and we’d expect this to impact our projects both in terms of size and speed. Let’s assess what the impact is by using a very simple example – an LED blinking every second.

Impact on Size

I created two programs: one in the Arduino IDE, and one in Atmel Studio. Both blinked an LED repeatedly at one second intervals – that was all:

Arduino Atmel Studio
 void setup() {
   pinMode(13, OUTPUT);
 }

 void loop() {
   digitalWrite(13, HIGH);
   delay(1000);
   digitalWrite(13, LOW);
   delay(1000);
 }
#define F_CPU 16000000UL

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

int main(void)
{

    DDRB |= (1<<0);
    while (1) 
    {
        PORTB |= (1<<0);
        _delay_ms(1000);
        PORTB &= ~(1<<0);
        _delay_ms(1000);
    }
}

After compiling them both, I looked at the size of the hex file that was generated. The Arduino-generated file (Blink.cpp.hex) was 2,918 bytes. The Atmel Studio file (blink.hex) was only 508 bytes – nearly six times smaller! Of course this difference is more noticeable with smaller projects, as the space that the libraries occupy is a fixed overhead – but it is an interesting indicator nonetheless.

Impact on Speed

Baldengineer - digitalWrite Benchmarking

One of the blogs that I enjoy reading is that of the Baldengineer – I recommend that you pop over for a visit. James, the author of the blog, carried out a great experiment a while back – he timed how long it took to turn an LED on using the Arduino IDE and then using Atmel Studio. I’m sure that you’d expect the difference to be significant given the code that you’ve seen – accessing memory to obtain pin mappings, catering for possible MCU states, testing conditional statements. These all add up, and result in many more clock cycles than a simple direct port manipulation. James measured the digitalWrite() performance, and approximated it at 100 clock cycles – or more than 60 times slower that the direct port manipulation. Make sure you take a look at the detailed post – it’s interesting reading!

In Summary

Hopefully this has given you a little more insight into the workings of the Arduino IDE – while not a perfect or comprehensive investigation, I think it illustrates the point pretty well. If you’re working with the Arduino IDE at present, you may at least understand a little of the trade-off that you’re making for the sake of ease-of-use – allowing you to decide which projects are best suited to Arduino and which are best suited to Atmel Studio. If you’re already working in Atmel Studio, and wondering what the benefits are to using cryptic coding over simpler function calls, then hopefully this has helped to reassure you. Either way, share your thoughts and experience in the comments below!

Go Beyond the Arduino
ebook - Arduino to AVR

I’ve just completed my brand new guide Arduino to AVR: Get Started in 3 Steps.
Get it now on Payhip for only $1.65.

Challenge yourself and learn how to gain the flexibility and additional control that the AVR microcontroller offers.

As a free bonus, get an ATmega328P pinout cheatsheet.

10 thoughts on “How Arduino digitalWrite Works – and why AVR is Faster

  1. I tought you Arduino to AVR guide would do the same as this guide. I wanted to see what happens behind the analogWrite() in arduino. I am disappointed, 2 euro down the drain.

    • Hi
      I’m sorry that it disappointed you – the intention of the Arduino to AVR guide was to get you started on the road from Arduino to AVR development. I did try to outline that in the description of the guide. I’m happy to refund you if you feel it was a waste – please drop me a mail on info(at)crash-bang(dot)com.
      Thanks for leaving a comment
      Andrew

      • It’s not a problem, i read the entire guide, most of it i already knew due to studying elektromechanica engineering. It’s not alot of money so I don’t need a refund. It’s kind that you would want to refund me. You put your time and effort into it so you earn it. Just like i said i hoped to get a little bit more info into what happend behind the arduino ide to get to know better how to program in atmel studio. I recently looked further into the arduino program code. Found the wiring files what makes analogwrite and digitalwrite work 🙂 Anyway, the guide might get handy sometime in the future. When I start building my own microcontroller boards.
        Greets,
        Wouter

      • how about the first three functions.
        digitalPinToTimer(pin);
        digitalPinToBitMask(pin);
        digitalPinToPort(pin);
        do they exist in mikroC for AVR for example.

  2. By writing hard-coded values directly into the hardware registers, you win in efficiency but you loose in readability and portability.

    I’ve published a tool that gives you readability, portability and efficiency. It provides an object-oriented interface for hardware programming that does not require a C++ compiler and produces fully optimized binary code.

    It is called HWA and you can find it here: https://github.com/duparq/hwa

    • Hi

      Thanks for sharing – looks interesting although haven’t had time to look at it in detail.

      Cheers
      Andrew

  3. Thanks, informative. For any other target, there are similar considerations, but one should read wiring_digital.c to know. For example, on Energia (the Arduino port to the MSP430 target) digitalWrite() is slightly different, and the target has more registers to configure port pins.

    Also, your code comparison is not quite fair, since you don’t disable PWM. If you have full knowledge of the rest of your program (you know that the PWM is never used), you can do that safely. Your conclusion is still valid, the code would be much smaller even if you disable PWM in your AVR version.

    You can reach a similar conclusion for many of the Wiring libraries: they are bloated by generality and safety, and you can often recode them to make them smaller.

Leave a Reply to AndrewR Cancel reply

Your email address will not be published. Required fields are marked *