Introduction: Fast DigitalRead(), DigitalWrite() for Arduino

About: Jack passed away May 20, 2018 after a long battle with cancer. His Instructables site will be kept active and questions will be answered by our son-in-law, Terry Pilling. Most of Jack's instructables are tuto…

On the Arduino, and all Atmel microcontrollers, processing is fast when using the Arduino IDE. But the input/output is very slow. If you have a time critical program digitalRead() and digitalWrite() can slow it down a lot. When writing this instructable I found out how much. When I first started I had eight LEDS. It worked fine the first time through but the longer it ran more mixed up the timing became. In order to keep it simple I switched to four LEDS to solve the problem.

It is possible to use lower level commands to greatly speed up the input/output.

There are three banks of pins on the Atmega 328 and 168 microcontrollers called B, C, and D.

  • The B bank is digital pins 8 - 13.
  • The C bank is the analog input pins.
  • Bank D is pins 0 - 7.

Each bank of pins has three 8 bit registers used to control it:

  • The DDR register is the data direction, 0 = input, 1 = output.
  • The PIN register is used to read the digital value of the pin.
  • The PORT register has two functions:
    • If the DDR register is set to output 0 sets the pin low and 1 sets it high.
    • If the DDR register is set to input 1 turns on the internal pull-up resistor.

Step 1: List of Registers to Control Pins

Each pin is one bit on the controlling registers.

Here is a list of the pins and the corresponding registers:

  • B0 = digital pin 8
  • B1 = digital pin 9
  • B2 = digital pin 10
  • B3 = digital pin 11
  • B4 = digital pin 12
  • B5 = digital pin 13
  • B6 is mapped to the crystal, do not use.
  • B7 is mapped to the crystal, do not use.
  • C0 = analog pin 0
  • C1 = analog pin 1
  • C2 = analog pin 2
  • C3 = analog pin 3
  • C4 = analog pin 4
  • C5 = analog pin 5
  • C6 = analog pin 6, available only on Arduino mini.
  • C7 = analog pin 7, available only on Arduino mini.
  • D0 = digital pin 0, used for serial communication, save it's state.
  • D1 = digital pin 1, used for serial communication, save it's state.
  • D2 = digital pin 2
  • D3 = digital pin 3
  • D4 = digital pin 4
  • D5 = digital pin 5
  • D6 = digital pin 6
  • D7 = digital pin 7

Sadly the Arduino does not have a bank that gives you unrestricted use of eight pins. I will explain the restrictions in the following steps.

( If you would like to have unrestricted use of all eight pins on a register try working with the Attiny84 chip.)

Step 2: Blink an LED

For these first programs all that is needed is a working Arduino, we will be blinking the internal LED on digital pin 13. Copy these programs into the Arduino IDE and upload them to your Arduino.

Here is the first program:

/********************************************************
 * setup() function
 *
 * Set B5 (Digital pin 13) to output by changing the DDRB
 * register instead of using pinMode().
 *******************************************************/
void setup()
{
  DDRB = B00100000;
}

/****************************************************************
 * loop() function
 *
 * Turn the LED attached to B5 (Digital pin 13) on and off
 * by changing the PORTB register instead of using digitalWrite().
 ****************************************************************/
void loop()
{
  PORTB = B00100000; //Turn LED on.
  delay(1000);
  PORTB = B00000000; //Turn LED off.
  delay(1000);
}

The "B" before the number tells the compiler to interpret the number as binary.

The bits are numbered 0 to 7, the right most bit is the zero bit (2^0).

It will also work with decimal numbers, use whatever is most convenient.

Here is an example with decimal numbers:

void setup()
{
  DDRB = 32;
}

void loop()
{
  PORTB = 32; //Turn LED on.
  delay(1000);
  PORTB = 0; //Turn LED off.
  delay(1000);
}

It will even work if the values are in variables, binary or decimal:

int on = B00100000;
int off = B00000000;

void setup()
{
  DDRB = on;
}

void loop()
{
  PORTB = on; //Turn LED on.
  delay(1000); 
  PORTB = off; //Turn LED off.
  delay(1000);
}

As mentioned in step 1 bits six and seven are mapped to the crystal, just leave them alone and you will be all right.

Step 3: Switch and LED and Using Pin Bank D.

Bank D controls pins 0 - 7, but pins 0 and 1 are used for serial communication. Most Arduino enthusiasts do not try to use these pins for anything else. Things can get weird if you mess with these pins. So for safety it is best to preserve the values of bits 0 and 1 in the DDRD and PORTD registers. This requires the use of logical AND and OR commands.

Each register is 8 bits numbered 0 to 7 from right to left. Bit 0 is 2^0, bit 1 is 2^1, etc.

A logical OR compares two bytes bit for bit and the result is 1 if either or the bytes is 1, if not the result is 0.

The vertical line (|) is the symbol for a logical OR.

Here is a truth table for a logical OR:

0 | 0 = 0
0 | 1 = 1
1 | 0 = 1
1 | 1 = 1
So if we OR        11001100
Against            00111100
The result will be 11111100

.

A logical AND compares two bytes bit for bit and the result is 1 only if both bits are 1.
The ampersand (&) is the symbol for a logical AND.

Here is a truth table for a logical AND:

0 & 0 = 0
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1
So if we AND       11001100
Against            00111100 
The result will be 00001100

In order to preserve a bit you can OR it against 0 or AND it against 1.

Follow along with the documentation in the program to see how this works.

Build the circuit shown in the diagram, you will need:

  • Arduino
  • Breadboard
  • LED
  • Resistor, 330-560 Ohm
  • Jumper wires

Copy this program into the Arduino IDE and upload it to your Arduino:

/*********************************************************
 * Demonstration using bank D pins 0 - 7 and preserving the 
 * values of pins 0 and 1 in the DDRD and PORTD registers.
 *
 * The anode of an LED is connected to pin 7 with 
 * a resistor in series connected to ground. 
 *
 * A pushbutton switch is connected to pin 2 and ground
 * and uses the internal pull-up resistor.
 *
 * The LED lights when the button is pressed.
 *
 *********************************************************/

/**********************************************
 * setup() function
 **********************************************/
void setup()
{
  // Set pin 2 to input and pin 7 to output
  // while maintaining the state of pins 0 and 1.
  // We don't care what happens to 3 - 6.

  DDRD = DDRD | B10000000;

  // The "|" means a logical OR.
  // We now know that bit 7 is high.
  // And we know bits 0 and 1 are preserved.
  // But we still are not sure of bit two. 

  DDRD = DDRD & B10000011;

  // We do a logical AND, now we know the status of all the bits.

  // A logical OR against zero or a logical AND against one
  // will not change the status of a bit.

  // This preserved the status of bits 7, 1, and 0.
  // Since bit 2 was ANDed against 0 we know that it is now clear.
  // The DDRD register is now where we want it.
 
  // Now we need to get the PORTD register set the way we want it.

  PORTD = PORTD & B00000011;

  // Bits 0 and 1 are preserved, all others are off.

  PORTD = PORTD | B00000100;

  // Bits 7 is off, the initial state of the LED.
  // Bit 2 is on, because pin 2 is an input turning it's bit
  // on in PORTD turns on the internal pull-up resistor.
}

/**********************************************
 * loop() function
 **********************************************/
void loop()
{
  // Read the PIND register.

  int button = PIND;

  // you now have the values of all eight pins in the PIND register
  // contained in a variable. The only pin we care about is pin 2.
  // So we do a logical AND on the button variable to isolate the
  // bit we want.
       
  button = button & B00000100;

  // Because of the internal pull-up resistor the pin will be high
  // if the button is not pressed, and low if it is.
  // So button will return either 2^2 (4) or zero if it is pressed.

  PORTD = PORTD & B00000111;

  // Turn LED off, and preserve bits 0 - 2.
 
  if(button == 0) 
  {
    PORTD = PORTD | B10000000;
  // turn LED on, and preserve bits 0 - 2.
  }
}

The digitalWrite() command will slow a program down a lot in a loop, but the pinMode() command is normally used only in the setup() function and run once. the program above will run just as well if you use a more standard setup() function, like this:

setup()
{
  pinMode(7, OUTPUT);
  pinMode(2, INPUT_PULLUP;
}

While using the DDRD register is not necessary it is nice to understand how it and the logical operations work.

Step 4: Is It Faster?

The first picture shows a resistor soldered onto an LED.

This is optional, but I found that having a bunch of these made up makes breadboarding a lot easier.

Solder a resistor to the cathode lead of some LEDs.

The cathode lead is the shorter negative (ground) lead. 270 - 560 ohms works good for on the RaspberryPi, for an Arduino use 330 - 680 ohm resistors.

I usually make them with 470 - 560 ohm resistors so they will work with both.

.

In the introduction I said that "It is possible to use lower level commands to greatly speed up the input/output."

Controlling registers directly eliminates a lot of extra code, and you can read or write all the pins on a bank with one command.

So now the fun part, a test that will show you some real speed.

Build the circuit according to the picture above, you will need:

  • Arduino
  • Breadboard
  • 6 LEDs
  • 6 resistors 330-560 ohm
  • jumper wires

Copy this program to the Arduino IDE and upload it to your Arduino, this is the fast program:

/***************************************************
 * Fast-counter.ino
 *
 * A fast binary counter works by controling the 
 * registers rather than using digitalWrite().
 ***************************************************/

/**************************************************
 * setup() Function
 **************************************************/
void setup()
{
  DDRB = B00111111; // Pins 8-13 set to output.
}

/**************************************************
 * loop() Function
 **************************************************/
void loop()
{
  for(int i=0;i<64;i++)
  {
    PORTB = i;
    delay(500);
  }
}

Let the program run for a while and you will notice the the speed does not change. Also note that when all the LEDs are lit and they all go out it is instant, they all go out at the same time.

Now copy the Slow-counter.ino into the Arduino IDE and upload it to your Arduino.

/*********************************************************
 * Slow-counter.ino 
 *
 * Another binary counter, but this one uses digitalWrite()
 * in a for loop, The longer it runs the slower it gets.
 *********************************************************/

unsigned long time[6];                  //Holds time for each LED.
int pin[6] = {8,9,10,11,12,13};         //Pin numbers for LEDs.
int toggle[6] = {0,0,0,0,0,0};          //Toggles for LEDs. (0 or 1)

/**********************************************
 * setup() function
 **********************************************/
void setup()
{
  for(int i=0;i<6;i++)
  {
    pinMode(pin[i], OUTPUT);            //Set LED pins as output.
    digitalWrite(pin[i], LOW);          //All LEDs are off at start.
    time[i] = millis();                 //Start timers for all LEDs.
  }
}

/**********************************************
 * loop() function
 **********************************************/
void loop()
{
  int interval[6]={500,1000,2000,4000,8000,16000};

  for(int i=0;i<6;i++)                   //For each LED in turn.
  {
    if((millis()-time[i]) > interval[i]) //Has time passed interval?
    {
      toggle[i] = !toggle[i];            //If so not toggle,
      time[i] = millis();                //reset time,
      digitalWrite(pin[i], toggle[i]);   //and toggle LED,
    }
  }
}

The first time all the LEDs go off you will notice that there is a slight delay and you can see them go off in sequence. The longer it runs the slower it gets.

.

If you like to work with Attiny chips:

Try running these programs on an ATtiny84 at one MHz. You will see a big difference.

If you have a digital multimeter that measures frequency write a program to blink one LED with delay(1). And compare the frequency when using digitalWrite(), to directly changing PORTB.

Step 5: Using Analog Pins As Digital

If you need more digital pins the analog input pins can be used as digital input/output pins.

Load up the Fast-counter program.

In the setup() function change DDRB to DDRC

In the loop() function change PORTB to PORTC

Now move the jumper wires from digital pins 8 - 13 to analog input pins 0 - 5.

Upload the program and you will now have six additional digital pins.