Speed read/write Arduino I/O

You may have noticed that reading and/or writing multiple I/O pins in an Arduino is quite time consuming, at least from a relative point of view. In this Quicky, you will learn how to speed up this process significantly.

Important note

In this tutorial, all examples will be written for the Arduino Uno, but any board that uses the ATmega328(P), should work as well since it has the same pinouts. For all other boards, you will have to figure out which “register” to use instead, but it will all be explained in this tutorial.

Classic slow route

Reading inputs from an Arduino is typically done by the command digitalRead(<pin>) and writing by digitalWrite(<pin>) with <pin> being the Arduino’s input/output pin number. Let’s start simple and read input pins 0 to 7 from an Arduino Uno. The code for this would be

/****************************************************************
      SETUP
****************************************************************/
void setup()
{
  pinMode(0, INPUT_PULLUP);  // Define pin 1 as input
  pinMode(1, INPUT_PULLUP);
  pinMode(2, INPUT_PULLUP);
  pinMode(3, INPUT_PULLUP);
  pinMode(4, INPUT_PULLUP);
  pinMode(5, INPUT_PULLUP);
  pinMode(6, INPUT_PULLUP);
  pinMode(7, INPUT_PULLUP);  // Define pin 7 as input
}

/****************************************************************
      LOOP
****************************************************************/
void loop()
{
  digitalRead(0); // Read state of pin 0
  digitalRead(1);
  digitalRead(2);
  digitalRead(3);
  digitalRead(4);
  digitalRead(5);
  digitalRead(6);
  digitalRead(7); // Read state of pin 7
}
//END

Or if you prefer iterations instead

void loop()
  for (int p = 0; p <= 7; p++)  {
    digitalRead(p);   // Read state of pins 0 to 7
  }
}

This method should look very familiar to you. But whether you use single lines of code to read all inputs, or iterations, the method of reading inputs remains the same – reading 1 input at a time – and the time it takes is (roughly) the same. So how many time would this take? Let’s find out, using this quick bit of code

// Define vars
unsigned long startTime;
unsigned long timeInterval;

/****************************************************************
      SETUP
****************************************************************/
void setup()
{
  pinMode(0, INPUT_PULLUP);  // Define pin 1 as input
  pinMode(1, INPUT_PULLUP);
  pinMode(2, INPUT_PULLUP);
  pinMode(3, INPUT_PULLUP);
  pinMode(4, INPUT_PULLUP);
  pinMode(5, INPUT_PULLUP);
  pinMode(6, INPUT_PULLUP);
  pinMode(7, INPUT_PULLUP);  // Define pin 7 as input

  Serial.begin(115200);	// Start serial communication
}

/****************************************************************
      LOOP
****************************************************************/
void loop()
{
  startTime = micros(); // Get the current time in microseconds
  for (int p = 0; p <= 7; p++)  {
    digitalRead(p);   // Read state of pins 0 to 7
  }
  timeInterval = micros() - startTime;  // Calculate the time it took to read 8 inputs
  Serial.println("Reading 8 I/O pins took " + (String)(timeInterval) + " microseconds");
  delay(1000);  // Wait a second
}
//END

Looking at the serial monitor, it appears on an Arduino Uno, it takes about 24 to 28 microseconds to read 8 input pins. That’s about 3 to little less than 4 microseconds per input.

17:40:51.647 -> Reading 8 I/O pins took 24 microseconds
17:40:52.666 -> Reading 8 I/O pins took 28 microseconds
17:40:53.638 -> Reading 8 I/O pins took 28 microseconds
17:40:54.628 -> Reading 8 I/O pins took 24 microseconds
17:40:55.658 -> Reading 8 I/O pins took 28 microseconds

Note that when using line by line code (digitalread(0) … digitalRead(7)) instead of using the iteration (for …), it even takes up to 32 microseconds, and just reading 1 input can take 4 to 8 microseconds. Off-course, this method for measuring the time it takes is not ideal, since reading the current time also takes time and is added to the calculation. But to get the idea, this will suffice for now. It also tells you the significance of the way you program things (line by line versus iterations). This has to do with the way the compiler handles your code.

Fast lane

To speed up reading these 8 input pins, run the following code

// Define vars
unsigned long startTime;
unsigned long timeInterval;
byte inputRegD;

/****************************************************************
      SETUP
****************************************************************/
void setup()
{
  // Set pins 0..7 as INPUTS using the register
  DDRD = B00000000;    // Note that 0 and 1 are TX and RX for serial comms.

  Serial.begin(115200);
}

/****************************************************************
      LOOP
****************************************************************/
void loop()
{
  startTime = micros(); // Get the current time in microseconds
  inputRegD = PIND;
  timeInterval = micros() - startTime;  // Calculate time it took to read 8 inputs
  Serial.println("Reading 8 I/O pins took " + (String)(timeInterval) + " microseconds"); 
  delay(1000);  // Wait a second
}
//END

Looking now at the serial monitor, it only takes 4 microseconds to read all 8 inputs. So what exactly does this code do differently than the classic way?

17:39:28.957 -> Reading 8 I/O pins took 4 microseconds
17:39:29.968 -> Reading 8 I/O pins took 4 microseconds
17:39:30.975 -> Reading 8 I/O pins took 4 microseconds

Using registers

In the fast code, registers were used to

  • define the pins 0 to 7 as input pins
    DDRD = B00000000 in the setup section
  • read input pins 0 to 7
    <variable> = PIND in the loop section

Let’s skip the setup() section for now and focus on the loop() section.
In short: The command PIND reads register D which holds the state of pins 0 to 7 on an Arduino Uno.

Basically, digitalRead(<pin>) does actually (almost) the same thing, but returns the state of only 1 pin, so you need to do this 8 times to get the state of all 8 input pins. 8 times at 4 microseconds (= 32µs), does that ring a bell?

// Define vars
unsigned long startTime;
unsigned long timeInterval;
byte inputRegD;

/****************************************************************
      SETUP
****************************************************************/
void setup()
{
  // Set pins 0..7 as INPUTS using the register
  DDRD = B00000000;    // Note that 0 and 1 are TX and RX for serial comms.

  Serial.begin(115200);
}

/****************************************************************
      LOOP
****************************************************************/
void loop()
{
  startTime = micros(); // Get the current time in microseconds
  myDigitalRead(0); // Read state of pin 0
  myDigitalRead(1);
  myDigitalRead(2);
  myDigitalRead(3);
  myDigitalRead(4);
  myDigitalRead(5);
  myDigitalRead(6);
  myDigitalRead(7); // Read state of pin 7
  timeInterval = micros() - startTime;  // Calculate time it took to read 8 inputs
  Serial.println("Reading 8 I/O pins took " + (String)(timeInterval) + " microseconds"); // 
  delay(1000);  // Wait a second
}

// Our own “digitalRead” function
bool myDigitalRead(int pin) {
  return bool(PIND >> pin & B1);
}
//END

Using a self written digitalRead-function, it takes now about 24 microseconds to read all 8 inputs. That is about the same as with the classic code.

17:51:18.927 -> Reading 8 I/O pins took 20 microseconds
17:51:19.946 -> Reading 8 I/O pins took 24 microseconds
17:51:20.953 -> Reading 8 I/O pins took 20 microseconds
17:51:21.960 -> Reading 8 I/O pins took 24 microseconds

As explained before, this is not the ideal method, and there will be tiny differences due to the way the code is written. But this should point out the clear difference between the two methods.

Working with registers

So how does this register thing work? It may look a bit overwhelming at first, but it is actually not that very hard. Let’s walk through this step by step.

If you look at the schematics of the Arduino Uno, and look for pins 0 to 7, you will find that pins 0 to 7 are labelled as PD0 to PD7. Notice the letter D, since this is the important part.
Hint: P stands for port, that is way this is usually referred to as Port registers.

ATmeag328(P) on an Arduino Uno
ATmeag328(P) on an Arduino Uno

As a side note: you can see that pins 8 to 13 are labelled PB0 to PB5 (notice the letter B) and the analogue pins A0 to A5 (which can also be used as digital I/O) are labelled PC0 to PC5 (notice here the letter C).

Since the Arduino Uno uses the microcontroller chip ATmega328(P), let’s also take a quick peek at its datasheet. Just ignore the pin numbers of the chip itself, since they differ from the pin numbers used on the Arduino Uno, but just focus on the labels next to the pins (PD0 to PD7, …).

ATmega328P IC from microchip
ATmega328P chip from microchip

You can find these datasheets on

To handle registers for digital inputs and outputs, there are three commands available

  1. DDR<register> to specify the pins as inputs or outputs
    similar to pinMode(<pin>, INPUT|OUTPUT)
    You would use this typically in the setup() section of your code
  2. PIN<register> to read digital inputs
    similar to digitalRead(<pin>)
  3. PORT<register> to write digital outputs
    similar to digitalWrite(<pin>)

So to read the register, use the command PIN followed by the “register name” – in this example, register D was used – so the command will be PIND

This will output a byte (8 bits) with the lowest bit (bit 0) being the state of pin 0, and the highest bit (bit 7) being the state of pin 7.

Bit number       7 6 5 4  3 2 1 0
Value example    0 0 1 0  1 0 0 1

The same method is used for reading registers B and C (although in this case only bit 0 to 5 make sense).

For all other Arduinos using different chips (like the ATmega16U2) the pin numbers on the Arduino board and the register names may vary, but the method will be the same. For example, the Arduino Mega uses the ATmega2560 chip, where pins A0 to A7 are located on port F.

ATmega2560 on the Arduino Mega2560 board
ATmega2560 on the Arduino Mega2560 board

So the read inputs A0 to A7, use the command PIN followed by the register name, which will be PINF

Reading and writing via registers

So how about another example to clear things up a bit

Tip: Writing a value to a variable can be done in a few ways

  • byte varDec = 255;  // As a decimal number
  • byte varBin = B11111111;  // As a binary number
  • byte varHex = 0xFF;  // As a hexadecimal number

For the next code to work, connect the following pins with jumper wires

From pin To pin
A0 8
A1 9
A2 10
A3 11
A4 12
A5 13

Knowing that A0 to A5 are the pins from register C, and pins 8 to 13 are the pins from register B, we can now read and write using the code below

// Define vars
byte counter = 0;

/****************************************************************
      SETUP
****************************************************************/
void setup()
{
  // Set pins 0..7 as INPUTS using the register
  DDRB = B00000000;    // Set pins 8 to 13 as input pins (register B)
  DDRC = B00111111;    // Set A0 to A5 as output pins (register C)
  Serial.begin(115200);
}

/****************************************************************
      LOOP
****************************************************************/
void loop()
{
  PORTC = counter;  // Write counter value to register C (A0..A5)

  // Debug to serial monitor
  Serial.print("Register C is now:" );
  Serial.print(PINC, BIN);    // Read register C (A0..A5)
  Serial.print(" and register B is now:" );
  Serial.println(PINB, BIN);  // Read register B (8..13)

  counter++;  // Increase counter value
  if (counter >= B01000000) {
    counter = 0;  // Reset counter if > 63
    Serial.println("--- Counter reset ---");
  }

  delay(1000);  // Wait a second
}
//END

Let’s skip the setup() section for now and focus on the loop() part.

A counter will count from 0 to 63, or binary from 0000 0000 to 0011 1111, and will then reset back to 0. That counter value is then written to register C with the command PORTC = <counter value> with the right most bit (bit 0) corresponding to pin A0, bit 1 to pin A1, …, bit 5 to pin A5 (bit 6 and 7 will not be used).

Then register B (pin 8 to 13) is read with the command PINB and since it is wired to register C, the values should be predictable (unless differently wired).

Also notice that register C (the output pins) is read using PINC. Just as you can read a pin with digitalread(<pin>) when it is set as an output pin with pinMode(<pin>) you can read pins via the register that are set as output pins as well.

Bit Register C Register B
0 (right most bit) A0 8
1 A1 9
2 A2 10
3 A3 11
4 A4 12
5 A5 13
6 (not used) (not used)
7 (left most bit) (not used) (not used)

And thus both registers should be the same. Let’s check the serial monitor

Register C is now:111100 and register B is now:111100
Register C is now:111101 and register B is now:111101
Register C is now:111110 and register B is now:111110
Register C is now:111111 and register B is now:111111
--- Counter reset ---
Register C is now:0 and register B is now:0
Register C is now:1 and register B is now:1
Register C is now:10 and register B is now:10
Register C is now:11 and register B is now:11

With the wiring done right, both registers show the same values.

Defining a pin as input or output

Now let’s finally take a look at the setup() part. Take a closer look at commands DDRB and DDRC You already know that the command DDR is the command used to define pins as input or output pins, followed by the register name (B, C, D).

The next thing you need to know is that a 0 will set the corresponding pin as an input pin – like pinMode(<pin>, INPUT) – and a 1 will set the corresponding pin as an output pin – like pinMode(<pin>, OUTPUT). Just as with reading and writing registers, the most right bit (bit 0) corresponds with the first bit in the register. In this example pin A0 in register C and pin 8 in register B.

A few more examples to clear thing up

  • DDRD = B11111111;  // Will set pin 0 to pin 7 as outputs on register D
  • DDRF = B00000000; // Will set pin 0 to pin 7 as inputs on register F
  • DDRE = B11110000:  // Will set pin 7 to pin 4 as outputs, pin 3 to pin 0 as inputs on register E

A few final tips

A final overview of the registers for the Arduino Uno

Bit Register B Register C Register D
0 8 A0 0 (also RX)
1 9 A1 1 (also TX)
2 10 A2 2
3 11 A3 3
4 12 A4 4
5 13 A5 5
6 6
7 7

You can mix register control with classic read, write and pinMode. The last executed command will be the final result.

PORTB = B000000;        // Set pins 8 to 13 to LOW stats
digitalWrite(8, HIGH);  // Set pin 8 to HIGH state
Serial.println("Pin 8 is now " + (String)digitalRead(8));

Executing the code above will show that pin 8 is now HIGH, and not LOW as set with the register, since the digitalWrite(8) is the last executed command before showing the state.

Mind RX and TX pins (0 and 1 for the Arduino Uno), just as you might corrupt your serial data by writing to pin 0 and 1 with digitalWrite, writing via registers does the very same thing. The same goes for SPI pins (11, 12, 13) and other protocols.

Reading and writing a single bit is done by a combination of bit-masking, shifting left and right, and using bit operators like

Operator Description Example
& AND (on bit level) B1 & B0 = B0
| OR (on bit level) B1 | B0 = B1
~ NOT (on bit level) ~B1 = B0

If you want to know more about bit-masking, shifting and operators, a Quicky will be published later covering this in detail.

Conclusion

Pros

Using registers can save up a lot of time and provides the ability to read several inputs and even more important, write several outputs at the same time. The last one can be very handy in time critical situation where it may be important to write all bits at an exact moment in time.

Another advantage of this method is that it can make your code much shorter.

Cons

No need to say that this is probably going to be a bit harder to debug, and may take some mathematical skills to filter out the required bits.

When using registers, it is also important to choose which pins you are going to use right. It won’t do you any good if your inputs (or outputs) are scattered over different registers.


References
Arduino Boards: Arduino Uno R3 – Arduino Mega2560
Arduino Software: Arduino IDE
Arduino Reference: digitalRead()digitalWrite()pinMode()Digital PinsPort RegistersATmega168 Arduino Pin Mapping