Arduino Communication Protocols Tutorial

arduino communication

arduino communication


Today we will be discussing arduino communication protocols.  Devices need to communicate with each other to relay information about the environment, express changes in their states, or request auxiliary actions be performed.  With any serious hobby electronics work, you’re bound to run into one or more of the main communication protocols in use, be it when working with different sensors, or with modules such as the ESP8266.  In this tutorial, we hope to introduce the standard communication protocols that electronic devices use and explain each of them in detail using Arduino Uno.

Binary and Number Systems

Inter-device communication occurs over digital signals.  Before we discuss communication protocols, we’ll first discuss how these signals are transmitted.  

In a digital signal, data is transferred as a sequence of high to low and low to high switchings that occur very rapidly.  These highs and lows in a digital signal represent 1s and 0s respectively that, when put together in sequence, carry information that can be interpreted by microcontrollers.  Ever heard of bits and bytes?  Individually, these 1s and 0s are bits, and when they are in groups of 8, they are called bytes!

A byte might look something like this: 10111001

As it turns out, this sequence of eight represents a number the same way a number like 597 represents five hundred ninety-seven.  Each of the digits occupies a place-value, and the 1 or 0 in that place-value indicates how many times the place-value is counted.  

In the example of 597, the 5 indicates that there are 5 hundreds, the 9 indicates that there are 9 tens, and the 7 indicates that there are 7 ones.  Put together, this means 5 hundreds + 9 tens + 7 ones (500 + 90 + 7)…or five hundred ninety-seven.  Since ones is 100, tens is 101, hundreds is 102, etc. this is called the base 10 system!  In the base 10 system, each digit can have the value 0 through 9 (0 through 10-1).

Now we return to our number 10111001.  We know this is the base 2 system because each digit has value 0 through 1.  We can then say that each digit is a power of 2, which would mean that 10111001 is actually 1 * 27 + 0 * 26 + 1 * 25 + 1 * 24 + 1 * 23 + 0 * 22 + 0 * 21 + 1 * 20, or 185 in base 10.  

As you can imagine, you can have number systems based on any number!  Some common ones in math are base 2, base 8, base 10, and base 16.  Mathematicians give these common number systems names for simplicity—base 2 is binary, base 8 is octal, base 10 is decimal, and base 16 is hexadecimal.  Each number system follows the same principal: each digit represents a number of times a power of that base is counted, and the value of each digit can only be between 0 and base-1 inclusive.

Knowledge of different base number systems is useful because bytes and data are often represented in different ways.  As you can imagine, it is easier to write out B9 (hexadecimal) than 10111001 (binary).  In software, binary numbers are prefixed with 0b, octal numbers are prefixed with a 0, and hexademical numbers are prefixed with a 0x.  Decimal numbers are not prefixed.  

Knowledge of converting between bases is also useful because representing numbers in binary often for some cool math tricks as well.  For the purposes of this tutorial, however, we’ll leave it at just that.  Check out the appendix to this tutorial below for articles on these tricks!

3 protocols: UART, SPI, and I2C

The electrical engineering community decided to standardize electronics around three communication protocols to ensure device compatibility.  Centering devices around a few protocols meant that designers would be able to interact with any device by knowing a few basic concepts about each communication protocols.  These three protocols, UART, SPI, and I2C, differ in their implementation, but ultimately serve the same purpose: transferring data at high speeds to any compatible device.  


The first communication protocol we’ll cover is Universal Asynchronous Receiver/Transmitter (UART).  UART is a form of serial communication because data is transmitted as sequential bits (we’ll get to this in a bit).  The wiring involved with setting up UART communication is very simple: one line for transmitting data (TX) and one line for receiving data (RX).  As you may expect, the TX line is used to for the data to send device, and the RX line is used to receive data.  Together the TX and RX lines of a device using serial communication form a serial port through which communication can occur.  

arduino communication

Figure 1. Hardware connection diagram for UART

The term UART actually refers to the onboard hardware that manages the packaging and translation of serial data.  For a device to be able to communicate via the UART protocol, it must have this hardware!  On the Arduino Uno, there is one serial port dedicated for communication with the computer the Arduino is connected to.  That’s right!  USB, which stands for Universal Serial Bus, is a serial port!  On the Arduino Uno, this USB connection is broken out through onboard hardware into two digital pins, GPIO 0 and GPIO 1, which can be used in projects that involve serial communication with electronics other than the computer.     

arduino communication

Figure 2. GPIO 0 and GPIO 1 are Serial RX and TX.  Any GPIO pin can be used as Serial RX or TX with the SoftwareSerial library

You can also use the SoftwareSerial Arduino library (SoftwareSerial.h) to use other GPIO pins as Serial RX and TX lines.  

UART is called asynchronous because the communication does not depend on a synchronized clock signal between the two devices attempting to communicate with each other.  Because the communication speed is not defined via this steady signal, the “sender” device cannot be certain that the “receiver” obtains the correct data.  Therefore, the devices break data into fixed-size chunks to ensure that the data received is the same as the data that was sent.  

The UART data packet looks something like this:

arduino communication

Figure 3. UART Data packet visualized / ©electric imp

Devices that communicate via UART send packets of pre-defined size that contain additional information regarding the start and end of the message and confirmation of whether the message was received correctly.  For example, to begin communication, the transmitting device pulls the transmit line low, indicating the start of a data packet.  Long-term, however, this means that UART is slower compared to a synchronized form of communication because only a portion of the data transmitted is for the devices’ applications (the rest is for the communication itself!)  

When implementing UART serial communication on most embedded platforms such as the Arduino, the user does not have to deal with communication at the bit level.  Instead, the platform often provides higher level software libraries that are the only aspect of the communication process the user has to deal with.  In the Arduino platform, users can use the Serial and SoftwareSerial libraries to implement UART communication for their projects.

Here’s a brief C++ reference for Arduino Serial and SoftwareSerial initialization and use.

Serial and SoftwareSerial Method Purpose Code Explanation
Constructor (SoftwareSerial only) Define the GPIO pins that will serve as the UART RX and TX lines SoftwareSerial comms (2 , 3); Defines a serial connection with RX line on GPIO 2 and TX line on GPIO 3.
begin Define the baud rate (transmission speed) for the serial connection in range 4800 to 115200 comms.begin(9600); Communication on the “comms” serial port will occur at 9600 baud.
print Write byte data converted into human-readable characters over serial connection comms.println(“Hello World”); Writes bytes equivalent to Hello World (human-readable characters)
write Write raw byte data over the serial connection comms.write(45); Writes byte with value 45.
available Evaluates to true when data is available over the serial connection if (comms.available()) Enter if statement if there is data available to read over the serial connection
read Read data from the serial connection; Reads from serial connection

One important aspect to note about UART communication is that it is designed for communication between only two devices at a time.  Because the protocol only sends bits indicating the start of a message, the message content, and the end of a message, there is no method of differentiating multiple transmitting and receiving devices on the same line.  If more than one device attempts to transmit data on the same line, bus contention occurs, and the receiving devices will most likely receive garbage unusable data!   

Furthermore, UART is half-duplex, which means that even though communication can occur bidirectionally, both devices cannot transmit data to each other at the same time.  In a project where two Arduinos are communicating with each other via a serial connection, for example, this would mean that at a given time, only one of the Arduinos can be “talking” to the other.  For most applications, however, this fact is relatively unimportant and not disadvantageous in any way.  

2. SPI

The next communication protocol we’ll cover is Serial Peripheral Interface (SPI).  SPI is different from UART in several key ways:

  • Synchronous
  • Follows a master-slave model, where there is one master device and multiple slave devices
  • More than two lines required for implementation

The hardware connection diagram for SPI is slightly more complicated, and looks something like this:

arduino communication

Figure 4. Hardware connection diagram for SPI


  • MOSI (“Master Out Slave In”): Data transmission line from master to slave
  • SCK (“Clock”): Clock line defining transmission speed and transmission start/end characteristics
  • SS (“Slave Select”): Line for master to select a particular slave to communicate with
  • MISO (“Master In Slave Out”): Data transmission line from slave to master


The first feature about SPI to note is that it follows a master-slave model.  This means that the communication relies on defining one device as a master, and other devices as slaves.  This creates a hierarchy between the devices signifying which device is effectively “in control” of the others.  We’ll discuss this master-slave model shortly when we describe what a sample communication between a master and slave device might look like.

We noted earlier that multiple slaves can be connected to a single master device.  The hardware diagram for such a system might look something like this:

arduino communication

Figure 5. Multiple-slaves connected to a single master

SPI does not require a separate transmit and receive line for each slave that is connected to the master.  One common receive line (MISO) and one common transmit line (MOSI), along with one common clock (SCK) line are connected between all the slaves and the master.  The master device decides which slave it is communicating with through a separate SS line for each slave.  This means that each additional slave that the master communicates with requires another GPIO pin on the master’s side.

SPI is synchronous, which means that communication between master and slave is closely tied to a clock signal (a square wave of fixed-frequency) defined by the master device.  Here we see one of the direct effects of the master-slave model.  The master device drives the communication by specifying the communication speed through the clock signal, and the slaves respond by communicating at that speed.  This definition of speed works for any communication speed the master decides on (within the maximum speed tolerable by the slave devices).  

In SPI, two specific features of the clock signal determine the start and end of data transmission: the clock polarity (CPOL) and clock phase (CPHA).  CPOL refers to the idle state (either low or high) of the clock signal.  To conserve power, devices will put the clock line at the idle state when not communicating with any slaves, and the two options available for this idle state are either low or high.  CPHA refers to the edge of the clock signal upon which data is captured.  A square wave has two edges (the rising edge and the falling edge), and depending on the CPHA setting, data is captured either on the rising edge or the falling edge.

There are four distinct combinations of CPOL and CPHA, that can be summarized in a table.

CPHA = 0

(“first edge” of clock signal)

CPHA = 1

(“second edge” of clock signal)

CPOL = 0

(idle 0)

  • “SPI mode 0”
  • Data capture begins when clock goes from 0 (idle) to 1 for the first time.
  • Data capture occurs on the rising edge of the clock signal (0→1)
  • “SPI mode 1”
  • Data capture begins when clock goes from 0 (idle) to 1 for the first time.
  • Data capture occurs on the falling edge of the clock signal (1→0)
CPOL = 1

(idle 1)

  • “SPI mode 2”
  • Data capture begins when clock goes from 1 (idle) to 0 for the first time.
  • Data capture occurs on the rising edge of the clock signal (0→1)
  • “SPI mode 3”
  • Data capture begins when clock goes from 1 (idle) to 0 for the first time.
  • Data capture occurs on the falling edge of the clock signal (1→0)

Wikipedia does a great job graphically representing the CPOL and CPHA settings and their relationship to the data being transmitted!

arduino communication

Figure 6. CPOL and CPHA settings visualized / ©Wikipedia


Now we’ll focus on SPI implementation on the Arduino using the Arduino as the master device (SPI.h).  The SPI digital pin connections for SCK, MOSI, and MISO are predefined on Arduino boards.  For the Arduino Uno, the connections are as follows:

  • SCK: GPIO 13 or ICSP 3
  • MOSI: GPIO 11 or ICSP 4
  • MISO: GPIO 12 or ICSP 1
  • SS: GPIO 10


Any digital pin can be also used for the SS pin.  To select the device, this digital pin must be driven low.  

arduino communication

Figure 7. The MOSI, MISO, and SCK pins are broken out on the ICSP headers as well as on GPIO 11, GPIO 12, and GPIO 13

Here’s a brief C++ reference for Arduino SPI initialization and use.

SPI Method Purpose Code Explanation
Constructor Define the clock speed, data bit order (most significant bit first or least significant bit first), and SPI mode SPI.beginTransaction
(SPISettings(14000000, MSBFIRST, SPI_MODE0));
Defines a SPI connection at 14MHz in SPI Mode 0 and data transmission occurring with the most significant bit first
digitalWrite Select the slave device attached to this GPIO pin digitalWrite(10, LOW); Drive the GPIO pin low to select the slave device.  Drive the pin high afterwards to de-select the slave.
transfer Transfer the byte to the selected slave SPI.transfer(0x00); Send a value of 0 as a byte
endTransaction Ends the SPI transaction (should be called after a digitalWrite(high) is called on the SS line) SPI.endTransaction(); Ends the SPI transaction

SPI is full-duplex, which means that communication always occurs bidirectionally, even if the application only requires data transfer occur in one direction.  SPI full-duplex data transfer is usually implemented with a shift register. (Check out the appendix for a tutorial on shift registers!)  This means that as each bit is read, the bits before it are shifted over by one place. The foremost bit is subsequently popped off and sent back through the SPI connection to the other device!

3. I2C

Inter-integrated circuit (I2C), pronounced either “i-squared-c” or “i-two-c,” is the final communication protocol we’ll cover in this tutorial.  Though its implementation is the most complicated of the three protocols, I2C addresses several drawbacks in the other communication protocols, giving it an advantage over the others in some applications.  These include:

  • The ability to connect multiple masters to multiple slaves
  • Synchronicity (just like SPI), which means higher speed communication
  • Simplicity: implementation only requires two wires and some resistors

At the hardware level, I2C is a two-wire interface—the only two wires required for an I2C connection are a data line (called SDA) and a clock line (called SCL).  The data and clock lines are pulled high in their idle states, and when data needs to be sent over the connection, the lines are pulled low through some MOSFET circuits.  For the purposes of this tutorial, we will not discuss the MOSFET operation inside I2C circuitry.  The important fact to take away is that the system is open-drain, which means that the lines can only be driven low by the devices.  Thus when using I2C in projects, it is crucial to include pull-up resistors (usually something like 4.7kΩ) to ensure that the line is actually pulled high in the idle state.  

arduino communication

Figure 8. I2C hardware connection diagram

I2C is unique because it solves the issue of interfacing with multiple slave devices through addressing.  Just like in SPI communication, I2C makes use of a master-slave model to establish the “hierarchy” of communication.  However, instead of selecting slaves through separate digital lines, masters select slaves through their unique byte addresses. These addresses might look something like this: 0x1B.  This means that connecting more slaves to a master will not require additional digital lines.  As long as each slave has a unique address, the application will still be able to differentiate the slaves on these addresses alone.  You can think of these addresses like names.  To call a slave’s functionality, the master merely calls its name, and only that slave responds.  

The address and data portions of the I2C communication line might look something like this.  

arduino communication

Figure 9. What the address and data portions of the communication might look like / ©

Notice the ACK and NACK bits on the communication line.  These bits denote whether or not the addressed slave responds to the communication—a way of periodically checking whether or not communication is occurring as expected.  These bits, of course, are unrelated to the address or data bits being sent, but in the grand scheme of the communication, they add very little extra time to the communication compared to the many start and end bits and pauses that occur in protocols like UART.  

I2C gives slaves freedom to decide what communication requests might look like.  Different slaves require different bytes to be written over in different orders over the SDA line to write and request information from them.  In some accelerometer modules, for example, bytes are written to denote which hardware register the master wants to read before any read requests are made.  For these specifications, the user must refer to the slaves’ datasheets for device addresses, register addresses, and device settings.

On the Arduino, I2C implementation occurs through the Wire library (Wire.h).  The Arduino can be configured as either an I2C master or slave device.  On the Arduino Uno, the connections are as follows:

  • SDA: Analog Pin 4
  • SCL: Analog Pin 5
arduino communication

Figure 9. I2C (Wire) SDA is Analog Pin 4 (A4).  SCL is Analog Pin 5 (A5).


Here’s a brief C++ reference for Arduino I2C initialization and use.

I2C (Wire) Method Purpose Code Explanation
begin Initiate the library and join the I2C bus as either a master or slave Wire.begin(); Joins the I2C bus as a master.  If an address is specified as a parameter to the method, the Arduino joins the bus as a slave with that address.  
beginTransmission For Arduino configured as a I2C master: initiate transmission with the slave that has the given address Wire.beginTransmission(0x68); Begins transmission with the slave that has the hexadecimal address 0x68
write Write the byte data over the I2C bus Wire.write(0x6B); Writes byte data of value 0x6B over the I2C bus
requestFrom Request a specified number of bytes from the slave with the given address; Has the option of releasing the I2C line or holding onto it for further communication

The requested bytes are put into a buffer to be subsequently read through calls.  

Wire.requestFrom(0x68, 6, true); Requests 6 bytes from the slave that has the address 0x68.  Releases the I2C line after the request is complete.

If the last parameter is false, the Arduino will hold onto the I2C line for further communication, not allowing other devices to communicate over it.

read Read bytes put into the buffer after transmissions from the slave device.

This method is called after a call to Wire.requestFrom; Reads one byte from the buffer (after a call to requestFrom) has been made.  To read two bytes from the buffer, this method must be called twice:


Wire.requestFrom(0x68, 2, true);;;

endTransmission Ends the current transmission; Has the option of releasing the I2C line or holding onto it for further communication Wire.endTransmission(true); End transmission and releases the I2C line.

Multiple masters can be connected over an I2C bus just by connecting their SDA and SCL lines to the bus’s lines.  However, only one master can communicate with slaves at a time, because having multiple devices attempting to communicate with each other would lead to bus contention.  Similarly, communication cannot occur bidirectionally from master to slave and from slave to master at the same time because that would also lead to bus contention.  This makes I2C half-duplex, just like UART!  

Finally, multiple master devices cannot communicate with each other over that same I2C bus.  In applications where multiple masters are connected to slaves, masters might communicate with each other over a separate bus or via a separate communication protocol.    

If you made it through this tutorial, you should have all the tools you need to work with the UART, SPI, and I2C communication protocols!  Check out some of our Arduino projects for further examples on using these protocols!  


Bitwise Operations in C:

Shift Registers:    

Pull-up Resistors:


Rahul Iyer
Rahul Iyer
Studying Electrical Engineering at UCLA, Rahul loves to work on electronics and robotics projects as a hobby. He is especially enthusiastic about electric vehicle technology and assistive robotics.

Check us out on Social Media