Home / Tutorials / Arduino Tutorial / How pinMode, digitalWrite and digitalRead Work
arduino digital input

How pinMode, digitalWrite and digitalRead Work

I believe what makes Arduino popular is how it makes microcontrollers easier to use. I remember the time when I have to spent hours coding a project and then spend money on programming hardware. Arduino has its limitations, but the platform helps create tons of electronic projects even for non-engineers.

Thus, I decided to create a series showing how the most common Arduino functions work. This is not only for greater insight but also to appreciate all the work that was done to make programming easier for a lot of people.

arduino digital input

For the first part of the series, I will be looking at three essential functions for dealing with digital inputs: pinMode(), digitalRead() and digitalWrite().

Introduction

For brevity, I will choose only one out of the 19 current official Arduino board: the Arduino UNO. The UNO runs on Atmel’s ATMega328p chip, whose pinout is below:

ATMega328p/Arduino pinout

As seen here, each pin (except the power pins) is assigned to a port and a pin number. Starting from digital pin 0 to analog pin 5, PORTDs come first, then PORTBs and then PORTCs.

No. Arduino Pin Number Port No. Arduino Pin Number Port No. Arduino Pin Number Port
1 D0 PORTD 9 D8 PORTB 14 A0 PORTC
2 D1 PORTD 10 D9 PORTB 15 A1 PORTC
3 D2 PORTD 11 D10 PORTB 16 A2 PORTC
4 D3 PORTD 12 D11 PORTB 17 A3 PORTC
5 D4 PORTD 13 D12 PORTB 18 A4 PORTC
6 D5 PORTD D13 PORTB 19 A5 PORTC
7 D6 PORTD Arduino pin numbers and corresponding ports, in sequence
8 D7 PORTD

This sequence makes sense later on.

Say you want to use digital pin 13 (PB5) as an output pin and digital pin 5 (PD5) as an input pin. You would normally do this:

From here on out, I will use this example sketch in our discussion.

The pinMode() Function

The pinMode() function accepts a pin number and mode as parameters:

This function is inside wiring_digital.c which is found in <Your Installation Folder>\Arduino\hardware\arduino\avr\cores\arduino. The Arduino platform is based on Wiring, hence the name of the file.

Here’s the function in full:

The pin Parameter

In the first two lines of the function, the eight-bit variables pin and mode are passed to macros digitalPinToBitMask and digitalPinToPort:

Both of these macros are inside Arduino.h found inside <Your Installation Folder>\Arduino\hardware\arduino\avr\cores\arduino and are defined as:

Here, the parameter pin is passed to P which is then used to select an element from the arrays digital_pin_to_port_PGM and digital_pin_to_bit_mask_PGM. Both these arrays are stored inside the microcontrollers program memory as the pgm_read_byte() function suggests. This makes sense since the ATMega328p has a lot of flash memory space compared to its RAM.

The elements of these arrays are viewable in the file pins_arduino.h which is in <Your Installation Folder>\Arduino\hardware\arduino\avr\variants\<Your Arduino Model>. There are different pins_arduino.h files for different Arduino boards. For the UNO, this file is inside the “standard” folder.

Here’s how the digital_pin_to_port_PGM array looks like:

This 20-element array contains the ports of the ATMega328p pins following the same sequence in the table above.  For example, doing this:

passes a value 13 to the macro:

which is now the value of P here:

which now returns the 14th element (count starts at zero) from the array.

This means that the variable port is now equal to a constant “PB”. This is a an alias for values defined in Arduino.h:

So “PB” is an alias for “2”. The others are there for Arduino boards with lots of ports and pins (the Arduino Mega contains up to PL).

Meanwhile, the array digital_pin_to_bit_mask_PGM has the following elements:

This also accepts the number 13 and, similar to digital_pin_to_port_PGM, will return the 14th element in the array, which is _BV(5).

_BV, short for bit value, is an AVR macro that sets a bit number in a byte. So _BV(5) in binary means:

where the 5th bit from the right, starting at zero is set. This is 0x20 in hexadecimal. So in this line

The variable bit is equal to 0x20.

As you see, both digital_pin_to_port_PGM and digital_pin_to_mask_PGM contain 20 elements inside the array. So what happens if you pass a number beyond the number of array elements? For example, what happens if you do this:

Since there’s nothing inside both arrays at position 21, these two will return with 0 bytes

Also, the value 0 is conveniently assigned an alias inside Arduino.h

Hence, inside the pinMode() function, you will see this line:

So passing a pin number beyond 20 bypasses pinMode() and will do nothing on the pins.

The mode Parameter

The mode can be INPUT, INPUT_PULLUP and OUTPUT and these are defined inside Arduino.h:

In AVR microcontrollers, controlling the behavior of a pin requires manipulating three registers: DDR, PORT and PIN. Each pin group has these three registers so registers DDRB, PORTC and PIND exist.

The DDR register is responsible for making a pin input or output. So if you write this in an Arduino code:

This makes pins PD4, PD5, PD6 and PD7 as output pins while PD0, PD1, PD2 and PD3 as input pins.

The PORT register clears or sets the actual pins. For example,

This sets the pin PD0 and clears all other pins.

The PIN register contains the digital value of the pins as inputs. They are used in the function digitalRead(). More on this later.

So the mode parameter inside pinMode() is passed to the macros

These macros are defined as

Again, the mode, which can be either 0x0, 0x1 or 0x2 is used to point inside the arrays port_to_output_PGM and port_to_mode_PGM.

The arrays are

Recall that this function

Returns “PB” equal to the number 2. This would point us to both DDRB and PORTB (3rd element in the arrays with starting count at 0). This means reg and port and these lines:

Which by the way are 8-bit pointers:

Will be equal to the contents of registers DDRB and PORTB respectively.

Now let’s see how these ports are manipulated based on the value of mode.

If a pin is to be an OUTPUT pin, the reg variable, which points to the DDR register, is OR’d with the “bit” value.

For our example:

Recall that the variable “bit” is equal to 0x20. So the DDRB register is equal to

This makes PB5 an output pin. Why do this instead of assigning 0x20 to DDRB directly? This method is known as masking. Through this, you can set (or clear) bit(s) in a byte, word, nibble while all other bits are unaffected.

Similary, if the pin is an input:

The corresponding DDR pin and PORT pin are manipulated like this:

For our example, this is equivalent to:

which uses AND masking and where 0xDF is the bitwise complement of 0x20.

Status Register

Notice this part:

This is found whether mode is INPUT, INPUT_PULLUP or OUTPUT. SREG is the ATMega328’s status register which is the register that holds information regarding the last Arithmetic Logic Unit operation, among other things.

What happened here is that the current SREG value is stored in a variable named oldSREG. This effectively preserves the status of the microcontroller’s CPU. Then, all interrupts are cleared via the command cli(). After the DDR value assignment, the previous content of the SREG is loaded back to itself.

The digitalWrite() Function

Now let’s look at the digitalWrite() function:

This function accepts pin and val as parameters. Some parts are similar to pinMode() except

Turning On/Off PWM

The digitalPinToTimer() function is defined as

Again, this reads an array, digital_pin_to_timer_PGM which contains:

The array is quite long, because the code anticipates the use of an ATMega8 microcontroller which has a different set of timers. So if you exclude #if defines for the ATMega8, you still have 20 elements in the same sequence as that in the table.

Looking back at the pinout of the ATMega328p, pins PD3, PD5, PD6, PB1, PB2 and PB3 are PWM pins. Pulse width modulation is possible through timer registers to which the ATMega328p has three kinds: Timer0, Timer1 and Timer2. Each timer uses at least two pins thus, you’ll see Timer0A, Timer0B, Timer1A, Timer1B, Timer2A, Timer2B.

Digital pin 13 is not a PWM pin, so this

will just return NOT_ON_TIMER which is another alias for 0. But if you will use a PWM pin say D5, this function will return TIMER0A which is an alias for “1”

and so the variable timer is now equal to 1. Back inside the digitalWrite() function, there’s this line:

Since timer is now not equal to 0, this line holds true. So, the turnOffPWM() function disables the PWM function of the timer register pointed by the pin number. This effectively makes the pin a digital output pin only.

Now for the rest of digitalWrite():

The pointer out returns the (address of) PORT register. In our example:

that’s PORTB. The contents of the status register is saved then interrupts are cleared.

If the variable val is LOW (an alias of zero), the PORTB register has a value that clears the pin. If the bit variable is 0x20, then

This masks the other pins except PB5 which is now LOW.

In contrast, if the variable val is HIGH (an alias of one), the PORTB register has a value that sets the pin. If the bit variable is 0x20, then

Similarly, this masks the other pins except PB5 which is now HIGH. In our example, this is the value of PORTB.

The digitalRead() Function

Here’s what’s inside the digitalRead() function:

The function only accepts a pin parameter. Here, we see this for the first time:

The portInputRegister() is a another macro:

Similar to other macros, this uses an array port_to_input_PGM with five elements:

So when you do this:

First it checks if this is a PWM pin and if it is, it turns off the timer associated with that pin. Then the portInputRegister() macro returns the appropriate PIN register. Recall that the PIN register is the data input register.

The value of the port variable is 4 and so in this case, the function returns the location of &PIND (fifth element in the array).

The bit value 0x20 masks the PIND register:

This checks if PD5 is high or low. If it’s high, then the digitalRead() function exits and returns HIGH, hence input, which is a 16-bit variable, is equal to 1. Otherwise, input is equal to zero.

Conclusion

Overall, this code snippet:

Is equivalent to this:

Both are short programs but the first one is easier to understand compared to the second (which may not work if coded in Arduino IDE). As seen in this article, it takes a lot of work to make microcontroller programming easier.

If you find this article useful, kindly drop a comment below. Thanks for reading!

Check Also

Arduino AD9833 signal generator

Using AD9833 Signal Generator with Arduino

Ever wondered how you can create sine, square or triangular waves with Arduino? Apparently, there’s …

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.