DigitalWrite: Use it to control any chip, LED or relay (and more). Understand the underlying code, and how to make it 17x faster! Can you use it with PWM pins?

DigitalWrite is the function that lets you control output from Arduino pins.

  • Easily Control your devices: LEDs, relays ICs etc.

  • Find out exactly how the Arduino digital write code works.

  • Understand how it interacts with the PWM function of some pins.

  • How to make it 17x faster (using macros).

Example Blink Sketch

Here is the blink example, that shows use of the digitalWrite function that blinks the built in LED:

void setup() {
  // Initialize digital pin LED_BUILTIN as an output.
  pinMode(LED_BUILTIN, OUTPUT);
}

// The loop function runs over and over again forever
void loop() {
  digitalWrite(LED_BUILTIN, HIGH);  // Turn LED on (HIGH volts)
  delay(1000);                      // Wait for a second
  digitalWrite(LED_BUILTIN, LOW);   // Turn LED off by (Low volts)
  delay(1000);                      // Wait for a second
}

Inside the microcontroller data is represented only as zeros and ones but outside the microcontroller the output is a voltage level:

  •  0 Volts represents a zero.
  •  5 Volts represents a one.

This function lets you control the output pin voltages so when you write a one to a pin the voltage changes to 5 Volts and when you write out a zero the voltage changes to 0 Volts.

Note: The high output voltage depends on the Specific Arduino you have - some use 3V3 as a high level. Arduino Uno and Nano use 5V.

It is a function that is used throughout the Arduino code because it separates the physical pin on the microcontroller from the software - it is an abstraction that enables easy interfacing to a pin.

This allows easy connection of external ICs.

Note: The oscilloscope waveforms below show how to speed up digitalWrite operation. However, this is not always needed. In fact most applications chug along quite happily using the standard digitalWrite function.

This faster macro operation is shown here so only so you know about it (see later macro definitions) - for instance you might want to speed up an operation due to time constraints in the application.

The oscilloscope shots below show the difference in using digitalWrite() on the left and using macros on the right. The period on the left is 7us while on the right it is ~0.4us.

Comparison of arduino uno digital write to macro speed

TIP: Macros (below) are ~17 times faster than digitalWrite().

Using digitalWrite in Arduino

For the function to work you must first set the pin direction to output. This is usually done once in the setup() function. All pins default to inputs on power up so if you don't set the pin as output it won't do anything.

Here's the basic use of the function.

pinMode(<pinnumber>, OUTPUT);    // Set the pin direction.

digitalWrite(<pinnumber>,HIGH);  // Set output pin high.

digitalWrite(<pinnumber>,LOW);   // Set output pin low.

Why is digitalwrite useful?

The function digitalWrite() is the cornerstone of the Arduino system creating a simple interface, with external Arduino connections described by referring to a number (the Arduino pin). The pin number is printed on the development board so its easy to see which output is being controlled.

Note: An Arduino pin is not the same as the chip pin number.

That does not sound too useful, but if you think about an Arduino Uno processor (14 I/O pins, with ATMega328p processor) and the Arduino Due (54 I/O pins, with Atmel SAM3X8E ARM Cortex-M3 CPU).  These processors are very different e.g. the Uno is an 8 bit processor and runs at 16MHz whereas the Due is a 32 bit processor running at 84MHz.

Using the digitalWrite function you can run the same code on different processors quite easily because you can select a pin to use e.g. put an LED on pin 5, and turn it on and off. In both the DUE and UNO versions (or another Arduino type board) you can use the same code to turn on and off pin 5 as a digital output.

This means code re-use is easy and does not require you to know anything about the underlying hardware (and how to access digital I/O).

Cost of using DigitalWrite


While digitalWrite is flexible, it has a small cost - speed. It takes time for the microcontroller to figure out which pin and port to use, and to convert the pin number. Processors are already fast, with the Uno at 16MIPS and Due at 100 MIPS. So losing a few clock cycles to the convenience of digitalWrite isn't typically an issue.

The only time the speed matters is when you need to toggle a pin very fast and precisely, like to control another device. Whether digitalWrite is fast enough then depends on the chip's specific timing requirements. For most purposes, the simplicity of digitalWrite outweighs the minor speed impact.

Uses for high speed digitalWrite

Here are some examples of situations where accelerating digitalWrite could potentially be useful:

  • Real-time control applications with tight deadlines, such as robotics or industrial automation systems. Precisely timing high-frequency digital output signals could be critical.

  • High-speed data streaming to or from an Arduino/microcontroller board. Faster digital I/O throughput could enable higher data rates.

  • Demanding display updates, like driving a high-resolution or high-refresh-rate display. Minimizing digitalWrite latency could avoid visual glitches or tearing.

  • Applications near the limits of an microcontroller's processing power where every CPU cycle counts. DigitalWrite optimization may help 'squeeze' more performance.

  • Testing/characterization where precise digital I/O timing needs to be measured and optimized and micro-optimizations matter.


So in summary, situations with real-time constraints, high data rates, or processing at the limit of the hardware capacity could potentially benefit most from accelerating digitalWrite calls.

DigtalWrite Advantages and Disadvantages

Advantages

Code is cross processor compatible.
Code is easy to understand.
Code controls a named pin on the board and is therefore easy to wire up.
Changing code to use different pins is trivial.

Disadvantages

Code is slower than accessing the ports directly.
Can not perform multiple bit read or write in a single action.

Using digitalWrite

You can either read from a pin, or write to a pin, but you can't do both at the same time as separate internal hardware is used for each operation. The processor needs to know how the pin is to be used by your program.

You use the function pinMode() to tell the processor to configure the pin for reading or writing.

Note: The default state of a pin is INPUT.

For writing, you setup the pin for digital output as follows:

const byte thispin 1;
pinMode( thispin, OUPTUT);

You can place the above code in the setup() function since you only set the pin direction at the start of the program and it usually remains the same. I say usually, since sometimes you may need to change the pin direction to allow some devices to operate e.g. Dallas 1 wire system is bidirectional on one pin. You can change the pin direction at anytime if you need to do so.

So How Does digitalwrite Work

For this explanation we will look at the Aruduino Uno code base (the code will be different for the Due and any other processor,  but the same principles will apply).

Digital write source code

Location:
C:/Program Files/Arduino/hardware/arduino/avr/cores/arduino/wiring_digital.c
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;
}

Digitalwrite internal structure

The first part of the code figures out three things:
  1. Is there a hardware PWM associated with the pin?
  2. The bit mask for the pin.
  3. The port that the pin is located on.

There there are two housekeeping operations:

  1. Invalid port so exit.
  2. Turn off the PWM output if the pin is PWM capable.

Then comes the actual desired operation of setting an output bit high or low.

  1. if val is low then set the output pin state to zero volts.
  2. if val is high then set the output pin state to high volts.

First the part that figures out ports and pins

The first part of this code is where the magic happens in translating the abstract pin number into a port and a bit mask. In addition there is also a timer parameter which is associated with PWM outputs.

	uint8_t timer = digitalPinToTimer(pin);
	uint8_t bit = digitalPinToBitMask(pin);
	uint8_t port = digitalPinToPort(pin);

Use of digitalPinToTimer

Arduino.h

#define digitalPinToTimer(P) ( pgm_read_byte( digital_pin_to_timer_PGM + (P) ) )

For the Arduino Uno R3 ports used as digital I/O are mapped from pin 0 to 13 as port D followed by port B.

  • pins 0 to 7 map to PD0 to PD7
  • pins 8 to 13 map to PB0 to PB5
If you look at the source code where digital_pin_to_timer_PGM is written:

C:\Program Files\Arduino\hardware\arduino\avr\variants\standard\pins_arduino.h

Note: The definition __AVR_ATmega8__ is for the ATmega8 Version of the chip that was used in the R0 version of the Arduino Uno. It is unlikely that you will see one these today; You can find a picture of one on the Wikipedia page here.

The key to understanding this code is the hardware configuration of the ATMega328p which has PWM outputs on:
  • pin 3, hwpin 5 (PD3),
  • pin 5, hwpin 11 (PD5),
  • pin 6, hwpin 12 (PD6),
  • pin 9, hwpin 15 (PB1),
  • pin 10, hwpin 16 (PB2),
  • pin 11, hwpin 17 (PB3).
The ATmega168 added PWM pins as PD3, PD5, and PD6 whereas the ATMega8 had PWM outputs only available on PB1, PB2, and PB3. The pinout of the ATmega328 should be the same as the ATmega168 as there are no definitions to separate them out in the code.

The issue here is that if a pin is being used as a digital output pin then it can not be used as a PWM output and the desire to use the digital output overrides PMW use.

The array allows the code to check if the pin can ever be a PWM output and which timer is associated with that pin.

Note: If it is a PWM pin then hardware dictates which timer is used. These are fixed in the ATMega328 hardware architecture.

If the pin has an associated timer (indicated by the array value) output then the function turns off the PWM output associated with that timer. i.e. if an array value is not equal to NOT_ON_TIMER then turn off the PWM output for that pin. For example

If you were using pin 2 then the array returns NOT_ON_TIMER - there is no PWM for pin 2. No action is taken.

If you were using pin 3 then the array returns TIMER2B indicating there is PWM on the pin and that it is using Timer2. The code then turns off the PWM output for pin 2 using the function turnOffPWM(timer).

Use of digitalPinToBitMask

Finds out the bitmask value for a specific Arduino pin.

Arduino.h:

#define digitalPinToBitMask(P) ( pgm_read_byte( digital_pin_to_bit_mask_PGM + (P) ) )

pins_arduino.h:
const uint8_t PROGMEM digital_pin_to_bit_mask_PGM[] = {
	_BV(0), /* 0, port D */
	_BV(1),
   ...
   };

_BV(n) is a macro that returns an 8 bit bit-mask (or bit value) and is defined by the compiler. The _BV(n) macro performs an equivalent bit shift operation but returns a value so there is no run time penalty in operating code i.e. there is no shift left command used - it is pre calculated as below. Examples:

_BV(0) returns 0x01  // operation was (1<<0)
_BV(1) returns 0x02  // operation was (1<<1)
_BV(7) returns 0x80  // operation was (1<<7)

So digitalPinToBitMask simply returns the bit value for a pin.

Use of digitalPinToPort

Finds out the port address for a specific Arduino pin.

Arduino.h:

 #define digitalPinToPort(P) ( pgm_read_byte( digital_pin_to_port_PGM + (P) ) )
digital_pin_to_port_PGM  is actually an array of byte sized port addresses so:
  • The first 8 values are PD (portD) - pins 0 to 7,
  • The next 6 values are PB (port B) - for pins 8 ro 13,
  • The next 6 values are PC (port C) - for pins 14 to 19 - These are the analogue pins.
TIP: Analogue pins can be used as digital pins if you set the pinMode to output or input. To set them back to analogue pins again, just perform an analogue read from the pin.

Finally, the part that does the actual work

The last part of the code does the actual port bit manipulation using inversion (~) and logical AND (&) and logical OR (|). These use the standard bit set or reset mask techniques.
 
	out = portOutputRegister(port);

	uint8_t oldSREG = SREG;
	cli();

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

	SREG = oldSREG;

Final part code explanation

First of all the 8 bit variable is set to the current value of the port in question.
 	out = portOutputRegister(port);
Then the current Status Register value (SREG) is saved, and interrupts are turned off. The status register contains the current global interrupt enable flag (b7). CLI is executed to turn interrupts off. When SREG is later updated with the oldSREG value, the state of interrupts will be preserved to the state that they were before executing CLI.
 	uint8_t oldSREG = SREG;
	cli();
If the function input value (val) is low then the specific bit is turned off. Anding the current port value with an inverted bit pattern (8 bits) sets the specific bit low (but only that bit).
 	*out &= ~bit;
Otherwise the bit is set high. Oring the current port value with the bit sets only that specific bit high.
 	*out |= bit;
Finally SREG is restored, restoring the interrupt enable flag to its previous state.

Why is digitalWrite so Slow?

DigitalWrite is slower because it is written to allow access to the Arduino pins and it does some housekeeping tasks PWM detection, port calculation, and bit mask calculation (as explored above).

However, digitalWrite is not that slow (~6us toggle)  and you can control most devices using digitalWrite. It only appears slow when you are trying to control more demanding interfaces. In this case use the fast macros shown below to get a ~20x speed increase.

How to speed up DigitalWrite

This section comes with a caveat - and that is: if you use the following for a speed increase then you are going into unprotected code area. This is simply the default programming method that software engineers/hardware designers are well used to.

What it simply means is that if you do stupid things then stupid things are going to happen!

The Arduino code above at the start of digitalWrite protects users from turning on the PWM outputs while at the same time trying to use them as normal digital I/O. In addition the code converts a simple number to a port and pin mask.

In a commercial engineered system pins are set for specific tasks and it up to the designer to use them appropriately. So if a pin is used as a PWM output to control a motor then it won't be used for anything else. So in that case you would not access the pin as normal digital I/O so you wont need protective code.

The protective code is there to allow inexperienced/careless engineering, so you really don't need it if you specify and setup the system as you want it to be. That means you can do away with these protections and gain a speed advantage. However note that digitalWrite is very convenient and in most cases it is fast enough.

The following two oscilloscope images show the difference in speed between digitalWrite() on left and simpler macro code on the right (They use the same scale: 1us per division):
Digital Write Speed 16MHz Arduino uno 1us per divisionMacro instead of digital write 16MHz Arduino Uno

These oscilloscope images are for an Arduino Uno running at 16MHz.

As you can see there is quite a speed difference. On the left the high period is about 3.5us high and 3.5us low. For the time it takes digitalWrite to toggle once, the macro has toggled about 18 times. Toggling on the right takes about 0.4us.

Zooming in on the right hand one:

Macro digital write 16MHz Arduino uno zoomed in
The scope settings were 0.5us/div and x5 multiplier. So it is high for 0.1us and low for 0.3us. The difference in M:S ratio is due to the jump instruction needed to return to the start of the while loop.

Example Sketch DigitalWrite and fast Macros


The code used for comparing digitalWrite() and macro versions is shown below:

#define setPin(b) ( (b)<8 ? PORTD |=(1<<(b)) : PORTB |=(1<<(b-8)) )

#define clrPin(b) ( (b)<8 ? PORTD &=~(1<<(b)) : PORTB &=~(1<<(b-8)) )

#define tstPin(b) ( (b)<8 ? (PORTD &(1<<(b)))!=0 : (PORTB &(1<<(b-8)))!=0 )


#define PIN_TST 12

void setup() {
  pinMode(PIN_TST,OUTPUT);
}

void loop() {

  while(1) {
    setPin(PIN_TST); // else
    clrPin(PIN_TST);

//    digitalWrite(PIN_TST,HIGH);
//    digitalWrite(PIN_TST,LOW);
  }
}

The fast macros work for pins 0 to 13 . To extend them to PORTC would need another condition similar to the following (not tested):

#define setPinABC(b) ( ((b)>13) ?   PORTC |=(1<<(b-13))  : \
                  ( (b)<8 ? PORTD |=(1<<(b)) : PORTB |=(1<<(b-8)) )  )

This adds another condition so will be slower. It does not check for an input greater than the maximum pin 19, so you could make it go wrong!

Note: For interrupt safe macros wrap the macros in the SRG code as in digital write. All it means is that outputs will not be interrupted i.e. won't be extended by an executing interrupt.

Some FAQs

Why digitalwrite doesn't work?

This is likely because you forgot to set the pin as OUTPUT using pinMode before using digitalWrite.

Do I need digitalwrite include?

No Arduino is different in that respect. In most compiler systems you have to include everything that you will need, but in Arduino this function is built in (as are many others - See Arduino reference). You only need 'includes' to incorporate external library functions.

Can I use digitalwrite in an interrupt?

Yes it is safe to use in an interrupt since it does not itself rely on any other interrupts.

Can I use digitalwrite in the setup function?

Yes

Can I use digitalwrite for multiple pins?

No: You have to individually write to each pin. If you want to write multiple pins at the same time they must be on the same port. The you use the bitwise operators to set or clear multiple pins at the same time (just set the bit mask to the pins you want to control).

My arduino digitalwrite is not 5v

The physical interface has limits (specifically current). If you load the pin with a large current drawing load the output voltage will drop even though the output is high. This is normal.

Either that, or you are using an Arduino with a different PSU level e.g. 3V3.

Can I use digitalwrite to drive a motor?

The output current is too low. You will need an interface circuit e.g. Darlington, MOSFET or relay or a motor driver chip (e.g. L293D) or shield with a motor driver chip on it.

What does digitalwrite return?

It returns void i.e. it does not return anything.


Comments

Have your say about what you just read! Leave me a comment in the box below.

Don’t see the comments box? Log in to your Facebook account, give Facebook consent, then return to this page and refresh it.




Privacy Policy | Contact | About Me

Site Map | Terms of Use