Memory Addressing
First, to understand why things are done this way, it should be known that a bool (boolean true/false) is only 1 bit. 1 for true, 0 for false. However, computers have an addressing system for memory, which cannot go directly to a single bit. An address usually goes to a 8 bit chunk of memory (a byte), which is also usually the same size as an int data type.
Think of it like trying to write a postal address to a room in a house. The address will take you to the house, but not inside. So, we can't just keep it in its own variable.
Furthermore, it would be wasteful to waste 7 bits for every bool declared. 8 bools can be put all into one integer - all next to each other in memory - to save space. How do you seperate them? How do you get just one bit out of a chunk of bits? With boolean logic!
Port Manipulation
To be as efficient, Arduino groups pins together into 8 bit "int" variables. Using boolean logic, you can perform operations on pins yourself, instead of using the built-in functions. This is called Port Manipulation. There are 3 groups of 3 variables. First, the pins are broken up into groups of 8 based on number.
- D: Arduino digital pins 0 to 7
- B: Arduino digital pins 8 to 13
- C: Arduino analog pins 0 to 5
Then, there are 3 types of registers which can perform different actions, each with a the corresponding letter replacing the '#':
- DDR#: Data Direction Register.
- Sets whether the each pin is read from or written to.
- PORT#: Port I/O
- Can write to each pin a high or low state, but can also be read
- PIN#: Pin Input
- Can read the current pin states of the pins in input mode.
Reading the pins
Pins are oranized in the register from lowest number at the right, to highest number at the left In this example, pins 11 through 13 are set as INPUT. Pins 11 and 13 are currently high at the moment, but pin 12 is low.
Pin: | NA | NA | Pin 13 | Pin 12 | Pin 11 | Pin 10 | Pin 9 | Pin 8 |
---|---|---|---|---|---|---|---|---|
Decimal Value | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
PINB | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 |
SelectP13 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
SelectP11 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
As you can see, if you were to print the value of PINC with pins 13 and 11 high but 12 low, the value with 32 + 8 = 40.
To select a single pin out of the PINC register, bitwise AND it with an int in the same place as the pin. So, PINB & SelectP13 = 13, or 0B00100000. Conditions in C++ will be true for any nonzero value. If Pin 13 were low, then the result would have been 0, or 0B00000000.
Programming Example
First, you have to set whether the pins are in read or write mode. Since this is often done exclusively in setup and only once per program, it is not as important to optimize this. It may be easiest to just use pinMode() to set the states instead of the more advanced ways described below with the DDR# register.
void setup() { pinMode(11, INPUT); pinMode(12, INPUT); pinMode(13, INPUT); } void loop() { //This condition is a single CPU operation
//pins 11,12, and 13 are all in the PINB group at the left
if ( PINB & 0B11100000 ) { Serial.println("Pin 11, 12, or 13 were high"); } //this condition requires dozens of operations if ( digitalRead(11) || digitalRead(12) || digitalRead(13) ) { Serial.println("Pin 11, 12, or 13 were high"); } }
In the above example, the slower second method each digitalRead must first figure out which register the pin is in, select it with a bitwise and, and return it to the condition. Then, each pin state must be ORd together.
Obviously, this case is not a typical one, but there are other uses. For example, one could write to several pins at once. This optimal method becomes extremely important when speed is an issue. When reading pins every few microseconds on interrupts, it can be beneficial to first store the whole register into a buffer, then do the required comparisons later in order to insure that all changes are being captured. Otherwise, data could be lost if the interrupt function is interrupted before it can complete.