Skip to content

Arduino Microcontroller: How to Use I2C, SPI, and UART

By Sebastian Günther

Posted in Microcontrollers, Uart, I2c, Spi, Arduino

The Arduino microcontroller is a versatile microcontroller, a true workhorse for many do it yourself projects. It has enough pins to connect several sensors and actuators. When building more complex system, you need to have a means for communicating with other microcontrollers or even single board computers.

In the last article, I explained the main protocols that are available on the Arduino and the Raspberry Pi: 1-Wire, I2C, SIP, UART. In this article, we will explore the libraries that are used to establish the connection with these protocols. Each library will be presented with its name, short description of its function, and a code example to get you up and running in minutes.

I2C

The I2C protocol is the preferred protocol for interacting with other MCUs (for brevity I will use MCU to refer to both microcontrollers and single board computers). The Arduino IDE comes bundled with the Wire.h library.

Lets see an example. The following program starts an IC2 master node.

#include <Arduino.h>
#include <Wire.h>

#define IC2_CLIENT 0x23

void setup() {
  Wire.begin();
  Serial.begin(9600);
}

void loop() {
  Wire.beginTransmission(IC2_CLIENT);
  Wire.write("Server Request");
  Wire.endTransmission();
  delay(3000);
}

The I2C client node will listen for incoming messages, and print them on the serial output.

#include <Arduino.h>
#include <Wire.h>

#define MY_I2C_ADDRESS 0x23

void setup() {
  Wire.begin(MY_I2C_ADDRESS);
  Serial.begin(9600);
}

void loop() {
  if (Wire.available()) {
    char c = Wire.read();
    Serial.print(c);
  }
}

This is just a basic example. The library also provides a mean to define non-blocking handler methods that are executed when data is available. And there are also methods to govern how the server requests to read data from its connected clients.

SPI

SPI connections can be made with the official SPI library Spi.h. This library provides extensive configuration options that reflect the complexity of the SPI protocol. It starts with configuring the maximum connection speed for all devices, whether messages will be exchanged with MSB or LSB, and how the voltage levels of the Clock and Data Channels are set.

Let’s take a look at an example, inspired by the official example.

#include <Arduino.h>
#include <SPI.h>

#define CLIENT_SELECT_PIN = 10
#define CLIENT_REGISTER_ADDRESS = 0x10;

void setup() {
  pinMode(CLIENT_SELECT_PIN, OUTPUT);
  SPI.begin();
}

void loop() {
  delay(1000);
  writeToSpiClient(0x10, millis());
}

void writeToSpiClient(char& buffer, int value) {
  delay(100);

  SPI.beginTransaction(SPISettings(14000000, MSBFIRST, SPI_MODE0));
  SPI.transfer(buffer,value);
  SPI.endTransaction();

  digitalWrite(CLIENT_SELECT_PIN, HIGH);
  delay(100);
}

The basic steps are:

  • Define the pin layout
  • Start the SPI server with SPI.begin()
  • Wrap any transaction in SPI.beginTransaction() and SPI.endTransaction()
  • Use SPI.transfer to send and receive data at the same time

UART

The final protocol is basic UART. Serial communication over the USB port always uses this protocol, and in addition, you can also wire another device using the RX TX Pins of the Arduino.

To open the UART connection, you need to define the baud rate, a technical term for the number of characters that can be send. Once established, you can read and write any character-based data.

The following program sends configures the baud rate to 9600. It will then send the "Hello UART" exactly one time to any connected client. Then, it will listen for any incoming data, and store it in a variable called msg.

#include <Arduino.h>
#include <stdbool.h>

string msg;
bool only_once = true;

void setup() {
  Serial.begin(9600);
}

void loop() {
  (if only_once) {
    only_once = false;
    Serial.send("Hello Client");
  }

  if (Serial.available()) {
    char c;
    while (c = Serial.read()) {
      msg += c;
    }
  }
}

Conclusion

Connecting your Arduino to other MCUs can be done with three different protocols: I2C, SPI and UART. These protocols are implemented as libraries to handle the details of the individual protocols. To my surprise, each protocol is handled by a built-in library. Using them follows the same schema: Opening a connection, then begin the transmission (direct, or select the receiver first), and closing the connection. Which method you use depends on the available protocols of the connected device, and also which transmission speeds you need.