Intro to SparkFun Qwiic I2C Shield for Arduino Nano

In this tutorial, you’ll explore the capabilities of the SparkFun Qwiic Shield for Arduino Nano—a modular shield designed to extend the functionality of the Arduino Nano. This shield enables seamless communication between your microcontroller and various I2c-based peripherals like sensors and some displays. It’s a standard connector type that’s been informally adopted by a few other hardware module manufacturers as well. We’ll cover everything from physical setup and orientation to practical demos using sensors available in the ITP shop.

It’s also worth noting that SparkFun’s Qwiic system is fully compatible with Adafruit’s STEMMA QT ecosystem, which uses the same connector and wiring convention. Beyond SparkFun and Adafruit, other companies such as Pimoroni, Seeed Studio, and Pololu have begun integrating this connector into their I2C sensor and breakout boards as well.

What You’ll Need to Know

Before diving in, you should be comfortable with basic Arduino programming and understand the fundamentals of I2C communication. If you need a refresher, review the I2C Communication Labs for in-depth details.

Things You’ll Need

For this tutorial, gather the following components (see Figures 1–6 for reference images):


Shield Orientation and Setup

Physical Orientation

Although the SparkFun Qwiic Shield for Arduino Nano is pinned out exactly like a standard Arduino Nano, it’s still important to verify that you have the correct orientation when mounting the shield. We often reference the 3.3V pin as a quick visual cue to ensure you align everything properly. The shield includes a configurable logic shifting circuit set by the IOREF jumper, which defaults to 3.3V (ideal for boards like the Arduino Nano 33 BLE). If you’re using a 5V Nano variant (such as the Arduino Nano Every), you’ll need to switch the jumper to 5V so that the logic levels match your board’s operating voltage.

Top-down view of two unconnected boards: an Arduino Nano 33 IoT on the left and a SparkFun Nano Qwiic Shield on the right. Black arrows label and point from specific pins on the Nano to the corresponding pins on the Qwiic Shield: 3.3V (top) and GND (bottom).
Figure 7. Pin alignment guide for powering the SparkFun Qwiic Shield using the Arduino Nano 33 IoT. Ensure that the GND and 3.3V pins are correctly connected to provide stable power and enable Qwiic-based I2C communication.


The Qwiic shield is designed to be mounted below the Nano with which it’s paired. To mount it below would require reversing the standard pin arrangement on the Nano (they are usually on the side of the board without components) and reversing the sockets on the shield.

Side view of an Arduino Nano 33 IoT mounted on a SparkFun Qwiic Shield, which is inserted into a white solderless breadboard. A USB cable powers the setup. A Qwiic cable extends from the shield to a connected breakout sensor (likely an APDS-9960 or similar), which rests off the breadboard surface. The setup is placed on a light-colored tabletop, with the wiring clearly visible.
Figure 8. Arduino on top of the Qwiic shield

Closer Look at the Adapter

The Qwiic adapter is populated with two 4-pin 1mm JST connectors. These connectors are polarized to prevent incorrect insertion, and the pin order follows the Qwiic standard: GND, 3.3V, SDA, and SCL. When mounting the adapter onto the Nano Qwiic Shield, ensure that the labeled side of the adapter aligns with the corresponding pins on the shield to maintain correct orientation.

Illustration of a Qwiic connector pinout on a red breakout board. The black 4-pin JST connector is shown with corresponding labeled lines pointing to each pin: SCL (yellow wire), SDA (blue wire), 3.3V (red wire), and GND (black wire). The connector is part of a SparkFun Qwiic-enabled board, with the “QWIIC” logo visible below the connector.
Figure 9. Qwiic connector pinout diagram showing the standard I2C wire order: SCL, SDA, 3.3V, and GND. The polarized JST connector ensures correct orientation, enabling plug-and-play communication between Qwiic-compatible devices without soldering. source
Close-up top-down view of a SparkFun Nano Qwiic Shield. The red breakout board has labeled black headers for inserting an Arduino Nano. A Qwiic cable is connected to the right-hand JST port, with four color-coded wires—yellow (SCL), blue (SDA), red (3.3V), and black (GND)—extending outward. The board is labeled with clear markings for power rails and I2C ports.
Figure 10. A SparkFun Nano Qwiic Shield with a Qwiic cable connected, showing the standard I2C wire orientation. 
Close-up view of a side-by-side comparison between a SparkFun Qwiic Shield’s female JST connector (left) and a matching male 4-pin JST Qwiic cable connector (right), held in place by a metal alligator clip. The connectors are aligned to demonstrate the proper orientation for connection. Four color-coded wires (black, red, blue, and yellow) extend from the cable.
Figure 11. Connector alignment for Qwiic interface: the 4-pin JST cable is shown in correct orientation for insertion into the Qwiic Shield’s port

Connection Diagram

Figure 12 shows the connections between the Qwiic Nano shield and a few typical components.

Simple wiring diagram showing a SparkFun Nano Qwiic Shield connected to an SHTC3 sensor (bottom left) and an SSD1306 OLED display (bottom right) using Qwiic-compatible 4-wire cables. The color-coded wires represent SCL (yellow), SDA (blue), 3.3V (red), and GND (black), maintaining consistent I2C bus connections.

Figure 12. Wiring diagram for a basic Qwiic I2C setup (Arduino below the shield is not shown in this diagram)

Note that while the Arduino Nano is not shown here, it is connected directly on top of the shield. Note also the placement of I2C communication pins (SDA and SCL). Ensuring the correct physical orientation prevents damage and guarantees reliable operation.

I2C Communication Overview

The SparkFun Qwiic Shield leverages the I2C protocol—a standard for connecting multiple devices with just two wires (SDA and SCL). This tutorial assumes that you’ve reviewed the ITP I2C Communication Labs or Related videos: Intro to Synchronous Serial, I2C for background on the protocol.

I2C Basics

  • Two-Wire Protocol: Communication occurs over SDA (data) and SCL (clock) lines.
  • Device Addressing: Each device on the I2C bus is identified by a unique 7-bit address.
  • Pull-up Resistors: These ensure proper voltage levels on the bus and are often integrated on breakout boards.

Demo Project: Reading Temperature and Humidity on an OLED Display


In this project, we’ll read data from SHTC3 (Humidity and Temp Sensor) and display it on an SSD1306 OLED screen. All components communicate via I2C and connect using Qwiic cables—no breadboards or soldering are required.

⚠️ Note: Many I2C sensors from providers like SparkFun (Qwiic) and Adafruit (STEMMA QT) come in two variants:

  • Qwiic/STEMMA QT-compatible versions, which feature JST connectors for plug-and-play wiring (see Figure 13 below) or
  • Standard breakout boards, which expose I2C pins (SDA, SCL, VCC, GND) but require jumper wires or soldering (see Figure 13 below)
Adafruit APDS9960 Proximity, Light, RGB, and Gesture Sensor - STEMMA QT / Qwiic
Figure 13. Adafruit APDS9960 Proximity, Light, RGB, and Gesture Sensor – with STEMMA QT / Qwiic connector source
Adafruit APDS9960 Proximity, Light, RGB, and Gesture Sensor
Figure 14. Adafruit APDS9960 Proximity, Light, RGB, and Gesture Sensor (without STEMMA QT / Qwiic connector) source

For this demo, we’re using the Qwiic-compatible versions, enabling quick, tool-free connections. However, you can also use standard versions of the same sensors as long as you connect them properly to the corresponding I2C pins.

Hardware Setup

Mount the Shield

  • Carefully align the Arduino Nano 33 IoT (or other Nano variant) with the SparkFun Qwiic Shield for Arduino Nano.
  • Ensure the 3.3V pins line up correctly to avoid damaging your board.

Connect the Sensors

  • Connect sensors to any available Qwiic port using Qwiic cables.
  • Daisy-chain sensors as needed for project layout.

Attach the OLED Display

  • If the OLED display includes a Qwiic connector, directly connect it to a free port or daisy-chain.
  • If not, use a Qwiic Jumper Adapter Cable to connect the necessary pins.

Power and Ground

The Nano’s USB port powers the setup, with the Qwiic Shield distributing 3.3V and GND.

Top-down view of an electronics setup featuring an Arduino Nano 33 IoT mounted on a SparkFun Qwiic Shield. The board is connected via Qwiic cable to an SHTC3 humidity and temperature sensor. The sensor is then wired to a small SSD1306 OLED display using jumper wires. The setup rests on a clean, white surface, and all components are connected with color-coded cables.
Figure 15. A compact I2C sensor-to-display demo using the Arduino Nano 33 IoT and SparkFun Qwiic Shield.

Install the External Libraries

In the Arduino IDE’s Library Manager (Sketch > Include Library > Manage Libraries…), install the following libraries and dependencies if needed.

Program the Microcontroller

In the demo code below, we begin by importing the necessary libraries and defining display parameters such as screen size and I2C address. In the setup() function, we initialize both the SHTC3 sensor and the OLED display, and configure display settings including text size, font, and color. In the loop(), the code reads humidity and temperature data from the sensor every three seconds and updates the OLED screen to show the latest values.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// Libraries for sensor and display communication
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_SHTC3.h>
 
// Instantiate sensor object
Adafruit_SHTC3 shtc3 = Adafruit_SHTC3();
 
// OLED display resolution and initialization
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1 // because the reset pin on the OLED is not being used
#define SCREEN_ADDRESS 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
 
void setup() {
  // initialize serial communication
  Serial.begin(9600);
  // wait 3 seconds if the serial monitor is not open:
  if (!Serial) delay(3000);
 
  // Initialize SHTC3 sensor, loop indefinitely if this fails:
  if (!shtc3.begin()) {
    Serial.println("Couldn't find SHTC3");
    // stop forever if the sensor is not available:
    while (true);
  }
  Serial.println("Found SHTC3 sensor");
 
  // Initialize OLED display, halt program if allocation fails
  if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("Couldn't find SSD1306 screen"));
    // stop forever if the display fails:
    while (true);
  }
 
  // Configure display settings
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.cp437(true);
}
 
void loop() {
  // make instances of the sensor elements from the sensor library:
  sensors_event_t humidity, temp;
 
  // Get fresh data from sensor
  shtc3.getEvent(&humidity, &temp);
 
  // Clear OLED display and set cursor to start
  display.clearDisplay();
  display.setCursor(0, 0);
 
  // Display temperature readings
  display.println("Temperature: ");
  display.print(temp.temperature);
  display.println(" °C");
  display.println("");
 
  // Display humidity readings
  display.println("Humidity: ");
  display.print(humidity.relative_humidity);
  display.println("% rH");
 
  // Update display with new data
  display.display();
 
  // Wait for 3 seconds before refreshing
  delay(3000);
}

Final Result

Animated GIF showing an Arduino Nano connected to a SparkFun Qwiic Shield, which is wired to a black rectangular sensor bar, a small SHTC3 temperature and humidity sensor, and an SSD1306 OLED display. The display is actively updating, showing the current temperature and humidity readings. All components are connected using Qwiic cables, and the setup is powered via a USB-C cable plugged into the microcontroller. The entire setup rests on a white tabletop.

Figure 16. Real-time environmental monitoring demo using an Arduino Nano with SparkFun’s Qwiic Shield. The SHTC3 sensor measures temperature and humidity, displaying live data on the SSD1306 OLED screen

Further Use Cases

For a more advanced setup, we can expand the I2C chain by adding more sensors or devices. In the demo below, there is an APDS-9960 gesture sensor and a BMP388 pressure sensor alongside the SHTC3 temperature and humidity sensor. The user can change the data display on the screen by simple hand movement. You can find examples for these sensors at this link.

A top-down view of an electronics setup featuring an Arduino Nano 33 IoT (far right) connected via jumper wires and Qwiic cables to multiple sensors and an OLED display. The components include three I2C Qwiic-compatible breakout boards from SparkFun (center to left), connected in series using Qwiic cables. At the bottom left, an SSD1306 OLED screen is also connected. The wiring is color-coded and neatly laid out on a light-colored surface.
Figure 17. Chain of I2C sensors and display connected to an Arduino Nano 33 IoT using SparkFun’s Qwiic system.
Animated GIF showing an SSD1306 OLED display updating live temperature and humidity readings from an SHTC3 sensor.  The SHTC3 (humidity and temperature sensor) is connected via Qwiic cable to another sensor, the APDS-9960, which detects proximity, gesture, ambient light, and color. All components are arranged on a light-colored tabletop and connected using color-coded Qwiic cables.
Figure 18. Live sensor data in action—temperature and humidity readings from an SHTC3 sensor are displayed on an OLED screen, while an APDS-9960 gesture and proximity sensor is also daisy-chained via Qwiic. This setup demonstrates how multiple I2C devices can work together seamlessly over a single bus.

Conclusion

The Sparkfun Qwiic Shield can simplify sensor integration and communication with Arduino Nano variants. It’s not the only way to connect IC components, but if you are, and if you’re using a Nano, it’s a handy tool. Whether you’re building a simple temperature monitor or a more complex interactive installation, understanding the practicalities of shield orientation, I2C communication, and connection strategies is key.

Lab: OLED Screen Display using I2C

Many common electronic devices feature small screens for status updates, control feedback, and so forth. These displays feature many different technologies. Lately, one of the most common is the OLED display. These displays are matrices of organic LEDs, each pixel being comprised of one to three LEDS. Small displays typically use either the SPI or I2C synchronous serial protocols. In this lab, you’ll use I2C to control a monochrome OLED display with an array of 128×64 OLEDs to display text and monochrome graphics. The Solomon Systech SSD1306 OLED driver IC is a popular and inexpensive display driver in lots of display breakouts.

For more on OLEDs, see this introduction from energy.gov. CNET provides this comparison of LCD vs OLED displays. This lab is adapted from material from this site.

What You’ll Need to Know

To get the most out of this Lab, you should be familiar with the basics of programming an Arduino microcontroller. If you’re not, review the Digital Input and Output Lab, and perhaps the Getting Started with Arduino guide. You should also understand asynchronous serial communication and how it differs from synchronous serial communication.

Things You’ll Need

Figures 1-6 list the components you will need.

Photo of an Arduino Nano 33 IoT module. The USB connector is at the top of the image, and the physical pins are numbered in a U-shape from top left to bottom left, then from bottom right to top right.
Figure 1. An Arduino Nano 33 IoT.
Three 22AWG solid core hookup wires. Each is about 6cm long. The top one is black; the middle one is red; the bottom one is blue. All three have stripped ends, approximately 4 to 5mm on each end.
Figure 2. 22AWG solid core hookup wires.
A short solderless breadboard with two rows of holes along each side. There are no components mounted on the board. The board is oriented sideways so that the long rows of holes are on the top and bottom of the image.
Figure 3. A short solderless breadboard.
Photo of an OLED display, approx. 2.5 cm x 2.5 cm
Figure 4. SSD1306 OLED display
Photo of two potentiometers
Figure 5. Potentiometer

The Circuit

The circuit for this lab consists of:

  • the microcontroller
  • a potentiometer. You can use any analog sensor you choose, however.
  • an SSD1306 OLED breakout board

There are many breakout boards that use the SSD1306 OLED: AdafruitSparkfun DFRobotCrystalfontzMakerFocus, Amazon, and many others. It typically comes in a 128×32 pixel version and a 128×64 pixel version. Solomon Systech makes other variations on this display as well, like the SSD1309 or the 1315. They come with different resolutions, different physical sizes, and different features.

Most of the SSD1206 displays have all blue pixels, but there are some models on the market have one section of yellow pixels and the rest blue. You can’t change the color of the pixels, so be aware of what you are buying and choose what you need. If you need a board with all blue pixels, don’t get the one with the yellow section.

Connect the display’s voltage and ground pins to your voltage and ground buses, and the I2C clock (SCL) and I2C serial data (SDA) pins to your microcontroller’s corresponding I2C pins as shown in Figure 6-7. The schematic, Figure 6, is the same for both the Uno and the Nano. For the Arduino Uno or the Arduino Nano boards, the I2C pins are pins A4 (SDA) and A5(SCL). This is the same connection for almost any I2C device.

Connect the potentiometer’s two outside connections to power and ground, respectively. Connect the middle pin (the wiper) to the Arduino’s pin A0. You’ll use the potentiometer to generate something to display on the screen.

Once you’ve got the circuit wired as shown in Figures 6-7, you’re ready to program the microcontroller.

 Breadboard view of a potentiometer and an SSD1306 OLED screen attached to a Nano 33 IoT
Figure 6. Breadboard view of a potentiometer and an SSD1306 OLED screen attached to a Nano 33 IoT. The potentiometer is connected to pin A0. The screen’s SDA pin is connected to pin A4 and the SCL pin is connected to pin A5.
Schematic drawing of a potentiometer and an SSD1306 OLED screen attached to a Nano 33 IoT.
Figure 7. Schematic drawing of a potentiometer and an SSD1306 OLED screen attached to a Nano 33 IoT. The schematic is the same for an Uno. The potentiometer is connected to pin A0. The screen’s SDA pin is connected to pin A4 and the SCL pin is connected to pin A5.

Program the Microcontroller

There are many libraries available for controlling the SSD1306 OLED screens. Adafruit’s SSD1306 library works well with all the SSD1306 displays, both the 128×64 and 128×32 models. Adafruit’s library is consistent with many of their other display libraries, and they make a lot of them. So it’s a good place to start. It doesn’t work with other SSD13xx models though. For example, Sparkfun makes a Micro OLED with the SSD1309, which has a 64×48 resolution. It requires a different library.

The u8g2 library by Oli Kraus, is intended as a universal monochrome display library for OLED, eInk, TFT, and other displays. It supports some SSD130x boards, but not all. It’s a pretty good library, but lacks some of the features of the Adafruit library. The examples included here use the Adafruit library. You’ll also need the Adafruit_GFX library which supports graphics across a number of small displays.

Import the Libraries

At the start of your sketch, import the libraries and set up a variable to hold the display driver instance like so:

1
2
3
4
5
6
7
8
9
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_GFX.h>
 
const int SCREEN_WIDTH = 128; // OLED display width, in pixels
const int SCREEN_HEIGHT = 64; // OLED display height, in pixels
 
// initialize the display:
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT);

Initialize the Display

In the setup, you need to check that the display is working. If it fails, it’s a good idea to stop and notify the user. Since there’s no user interface in this basic sketch, you’ll use the Serial Monitor.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void setup() {
  // initialize serial and wait for serial monitor to open:
  Serial.begin(9600);
  if (!Serial) delay(3000);
  // first parameter of begin() sets voltage source.
  // SSD1306_SWITCHCAPVCC is for 3.3V
  // second parameter is I2C address, which is
  // 0x3C, or 3D for some 128x64 modules:
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println("Display setup failed");
    while (true);
  }
  Serial.println("Display is good to go");
}

You can print to the display by giving it a text size and cursor position and then using .print() or .println(). The loop below reads analog input 0, then prints the time in seconds on the first line and the sensor reading on the second.

The function which actually updates the display is called .display(). All of the other functions update a buffer in the microcontroller’s memory. So you can make all the changes you want with commands like .print(), .setCursor(), .clearDisplay(), and so forth, but you will only see the changes when you call .display(). This is typical of many display libraries.

In the code below, you’ll clear the display, then set the size to twice the default, then move the cursor to the top left. Then you print the seconds since the sketch started, move the cursor down a line, and print the analog 0 sensor reading. Finally, you push it all to the display using .display():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void loop() {
  int sensorReading = analogRead(A0);
  // clear the display:
  display.clearDisplay();
  // set the text size to 2:
  display.setTextSize(2);
  // set the text color to white:
  display.setTextColor(SSD1306_WHITE);
 
  // move the cursor to 0,0:
  display.setCursor(0, 0);
  // print the seconds:
  display.print("secs:");
  display.print(millis() / 1000);
 
  // move the cursor down 20 pixels:
  display.setCursor(0, 20);
  // print a sensor reading:
  display.print("sensor:");
  display.print(sensorReading);
  // push everything out to the screen:
  display.display();
}

Here’s a link to the full sketch.

The SSD1306 is a monochrome display, but you still have to set the color to see anything display, because the library defaults to no colors. For a list of what SSD1306_WHITE and the other constants in the library mean, see this link.

Fonts

The default font of the library is not terribly attractive, and gets blocky when you increase the size. But you can add fonts. The Adafruit_GFX library includes many fonts compatible with lots of their display libraries. Search for this library in the library manager and include it to use the fonts. They have a good tutorial on using fonts as well. The short version is as follows. First, include the font you want to use with your library includes like so:

1
#include<Fonts/FreeSans9pt7b.h>

Next, in your setup() or whenever you want to switch fonts, use the .setFont() function like so:

1
display.setFont(&FreeSans9pt7b);

The text will now appear using your chosen font. Here’s a link to an example using one of the Adafruit_GFX fonts.

One of the nice things about using custom fonts is if you pick the right size for your screen, you don’t need to use .setTextSize(), so you can avoid pixelating your font. Note that custom fonts generally measure pixel height from the baseline. So, to position a 16-point font at the top of the screen, for example, position it with a few pixels at the top to spare: at (0, 20), not (0,0).

You can also use more custom fonts by generating them from this OLED display custom font generator. For “Library Version” choose “Adafruit GFX Font”. This will generate a custom header file that you include with your sketch. Make a new tab in the IDE, give the tab a name like font.h, and include it at the top of your code with the other library header files. The font name will be at the bottom of the header file. For example, if you generated the Dialog Font, Plain, 16 point, the font name would be Dialog_plain_16 and you’d include it just like the one above, like so:

1
display.setFont(&Dialog_plain_16);

The text will now appear using your custom font. Here’s a link to an example using a custom-generated font.

Graphics

There are many libraries for graphics on small displays. Typically these displays don’t have a fast refresh rate, so you can’t exactly create fast action games on them, but they can be good for simple graphic informational displays. Different display libraries will have slightingly different naming schemes for their graphics, but you can usually count on being able to draw points, lines, rectangles, circles, and sometimes rounded rects and triangles. You can generally also count on functions like drawRect() vs. fillRect() depending on whether you want a filled rectangle or not. All libraries will include methods for setting a color, clearing a screen, and filling the screen with a color. Most will also include commands for rotating the graphics display. Here’s the Adafruit_GFX library graphics primitives tutorial as an example.

Remember, all of your commands are only executed when you call .display().

Here’s a link to a graphic example that graphs the sensor reading. If you haven’t already included the Adafruit_GFX library, you will need to for this.

Displaying QR Codes

One way you can use the graphic capabilities of any small display is to show QR codes. QR codes can contain any string of text you want in a machine-readable form. Though we’re used to generating QR codes to supply web addresses (URLs), you can also send plain text strings, phone numbers, passcodes or other test-based information as well. On microcontroller-based devices, they can be a way to transfer identifying information like IP or MAC addresses, Bluetooth LE service UUIDs, or anything else your mobile phone or tablet might need to know about your microcontroller device in order to communicate with it.

QR codes can be generated from a text string and translated to a microcontroller display using Richard Moore’s qrcode library for Arduino and the graphics library for your particular display.

Like any library, you need to include the qrcode library at the top of your code like so:

1
#include <qrcode.h>

Determining QR Code Block Size

A QR code’s block size is the height and width of each of the blocks in the code. It depends size depends on the number of blocks per side. The length of the string you plan to display and a few other factors affect the block size. Here’s a table which lays out how the string length, error correction, and QR code version affect the the pixel dimensions of a QR code. Here’s a simpler version in the documentation for the qrcode library. A long string or higher levels of error correction can result in largee QR codes than you can fit on a small display. The library’s .size() function can give you the number of blocks in your QR code.

QR Codes need a “quiet zone” around the block in order to be readable to another device, so you can’t fill your display’s entire length or width with a QR code.

Here’s a good rule of thumb to get a block size that you can fit on your display:

  • Set the QR version and the QR error level
  • Start with an arbitrary offset. The larger your offset, the smaller the QR code can be.
  • Subtract twice the offset width (also arbitrary) from the display’s smallest dimension (64px, in the case of the SSD1306)
  • divide by the qrcode.size

Here’s a function to generate a QR code. It will take a String object, the QR code version you want to use, and the QR code error level you want, then it will generate a QRCode object, and display it on the display using the display library’s .fillRect() function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void displayQrCode(String message) {
  // Create the QR code
  QRCode qrcode;
  int qrVersion = 3;
  int qrErrorLevel = ECC_MEDIUM;
  int offset = 2;
  int backgroundColor = SSD1306_BLACK;
  int foregroundColor = SSD1306_WHITE;
 
  // allocate QR code memory:
  byte qrcodeBytes[qrcode_getBufferSize(qrVersion)];
  // initialize the QR code text:
  qrcode_initText(&qrcode, qrcodeBytes, qrVersion, qrErrorLevel, message.c_str());
  // calculate the QR code block size:
  int blockSize = (display.height() - (offset * 2)) / qrcode.size;
  // fill the screen with the background color:
  display.fillScreen(backgroundColor);
 
  // read the bytes of the QR code and set the blocks light or dark, accordingly:
  // vertical loop:
  for (byte y = 0; y < qrcode.size; y++) {
    // horizontal loop:
    for (byte x = 0; x < qrcode.size; x++) {
      // calculate the block's X and Y positions:
      int blockX = (x * blockSize) + offset;
      int blockY = (y * blockSize) + offset;
      // read the block value from the QRcode:
      int blockValue = qrcode_getModule(&qrcode, x, y);
      // set the default block color:
      int blockColor = backgroundColor;
      // if the block value is 1, set color to foreground color instead:
      if (blockValue == 1) {
        blockColor = foregroundColor;
      }
      // display the block on the screen:
      display.fillRect(blockX, blockY, blockSize, blockSize, blockColor);
    }
  }
  // refresh the display here, after the nested loops:
  display.display();
  // print the message:
  Serial.println(message);
}

Here’s a link to a full sketch to generate QR codes on an SSD1306 display. Upload it to your Nano, then open the serial monitor and enter a string to display. Then scan the QR Code with your phone’s QR scanning app. You should be able to read short strings of text, up to 50 or so characters. You can also send URLs, phone numbers, or anything else your phone’s QR code reader can interpret.

The shorter the string, the easier it is for your QR code scanner to read it. This is why many URLs you see on QR codes in public use URL shorteners.

Conclusion

Even though the SSD1306 is a small, low resolution monochrome display, it packs a lot of potential for the price. Adding an informational display can improve many devices, and by adding a QR code, you have the possibility to add many more features through the web, Bluetooth, and more. For more on microcntroller displays, see this repository.

Lab: I2C Communication with a Time-of-Flight Distance Sensor

Introduction

In this lab, you’ll see synchronous serial communication in action using the Inter-integrated Circuit (I2C) protocol with a time-of-flight distance sensor and a microcontroller.

Many different sensors on the market use the I2C protocol to communicate with microcontrollers. It is the most common way to connect to advanced sensors these days. The VL53L0X used in this lab is typical of an I2C sensor, so the principles covered here will help you when working with other I2C sensors as well. Much of this lab is adapted from the I2C lab on the APDS-9960 Color, Light, and Gesture Sensor.

Related videos: Intro to Synchronous Serial, I2C

What You’ll Need to Know

To get the most out of this Lab, you should be familiar with the basics of programming an Arduino microcontroller. If you’re not, review the Digital Input and Output Lab, and perhaps the Getting Started with Arduino guide. You should also understand asynchronous serial communication and how it differs from synchronous serial communication. You should also read the notes on distance sensors to learn more about time-of-flight, or VCSEL sensors.

Things You’ll Need

Figure 1-3 are the parts that  you need for this lab.

Three 22AWG solid core hookup wires. Each is about 6cm long. The top one is black; the middle one is red; the bottom one is blue. All three have stripped ends, approximately 4 to 5mm on each end.
Figure 1. 22AWG solid core hookup wires.
Photo of an Arduino Nano 33 IoT module. The USB connector is at the top of the image, and the physical pins are numbered in a U-shape from top left to bottom left, then from bottom right to top right.
Figure 2. Arduino Nano 33 IoT or other Arduino board
Figure 3. A VL53L0X distance sensor breakout board.

Sensor Characteristics

The sensor used in this lab, an ST Micro VL53L0X sensor, is an integrated circuit (IC) that can read the distance out to about 2000mm. It uses a 940 nm VCSEL emitter (Vertical Cavity Surface-Emitting Laser) that reflects off the target to determine the distance. There are breakout boards available for this sensor from multiple vendors, including Adafruit and Pololu (all available through Digikey, among others) .

I2C Connections

Connect the sensor’s voltage and ground pins to your voltage and ground buses, and the I2C clock (SCL) and I2C serial data (SDA) pins to your microcontroller’s corresponding I2C pins as shown in Figure 4-6. The schematic, Figure 4, is the same for both the Uno and the Nano. For the Arduino Uno or the Arduino Nano boards, the I2C pins are pins A4 (SDA) and A5(SCL). This is the same connection for almost any I2C sensor.

Some I2C sensors also have a few other pins:

  • an interrupt pin, used to signal the microprocessor when a reading is ready.
  • a shutdown or reset pin, used for powering down or resetting the sensor.

For this exercise, you won’t be using interrupt or shutdown pins.

What are Qwiic/Stemma/Grove/Gravity?

In addition to the standard I2C connections, Sparkfun and Adafruit use a connector called Qwiic which connects the I2C, power, and interrupt connectors all in one cable, eliminating the need for soldering. It’s a Sparkfun brand name. However, you’ll need a Qwiic adapter shield to use it. Adafruit have a similar brand called Stemma, Seedstudio uses Grove, and DFRobot uses Gravity. They all support I2C and have custom solderless connectors, though their connectors are not compatible with each other. To maintain compatibility, stick with the I2C header pins.

Schematic view of an Arduino attached to an VL53L0X sensor.
Figure 4. Schematic view of an Arduino attached to an VL53L0X sensor. This is the typical schematic for most I2C sensors.
Breadboard view of an Arduino attached to a VL53L0X sensor.
Figure 5. Breadboard view of an Arduino attached to a VL53L0X sensor. This shows an Adafruit breakout board, but other companies’ boards use the same pins: voltage, ground, SDA, and SCL.
Breadboard view of a VL53L0X distance sensor breakout board connected to an Arduino Nano 33 IoT
Figure 6. A VL53L0X distance sensor breakout board connected to an Arduino Nano 33 IoT. This shows an Adafruit breakout board, but other companies’ boards use the same pins: voltage, ground, SDA, and SCL.

The circuit is now complete, and you’re ready to write a program to control it. One of the advantages of the I2C synchronous serial protocol (as opposed to the SPI protocol) is that you only ever need two wires for communication to one or multiple devices.

How I2C Sensors Work

I2C devices exchange bits of data whenever the shared clock signal changes.  Controller and peripheral devices both send bits of data when the clock changes from low to high (called the rising edge of the clock). Unlike with SPI, they cannot send data at the same time.

The Vl53L0X has a series of memory registers that control its function. You can write to or read from these registers using I2C communication from your microcontroller. Some of these registers are writable by the controller so that you can configure the sensor. Some registers are configuration registers. Writing to them sets the sensor’s characteristics. For example, you can configure whether the sensor is in high speed mode, high sensitivity mode, or long distance mode. Other memory registers are read-only. For example, when the sensor has read the proximity of an object, it will store the result in a register that you can read from the controller. The details of the chip’s registers can be found in the sensor’s datasheet.

How I2C Bits are Exchanged

Most of the time, you never have to think about how the bits of an I2C message are exchanged. If so, then you might want to skip to the the next section on I2C Libraries. For the low-level details, read on:

I2C devices exchange data in 7-bit chunks, using an eighth bit to signal if you’re reading or writing by the controller or for acknowledgement of data received. The top seven bits of a byte are the data bits, and the bottom bit is the read/write bit. To get the distance from the VL53L0X, your controller device sends the sensor’s address (a 7-bit number, for this sensor it’s 0×29) followed by a single bit indicating whether you want to read data or write data (1 for read, 0 for write). That means the address byte is 0x53 for read, 0x52 for write access. Then you send the memory register in which the sensor’s ranging data is stored. The sensor then sends the value of that register back to you. For this particular sensor, STMicro wraps the control register documentation in an abstracted API, described in this document. The section labeled “Define Registers” lists all the registers and their addresses. The RESULT_RANGE_STATUS register is the register that holds the latest distance reading.

I2C and the Wire Library

The Wire library, which is built into the Arduino IDE, is the main library for accessing I2C communication. However, most Arduino-compatible I2C device libraries incorporate this library, but don’t expose it directly in their APIs. This makes things simpler for the user. Instead of having to work out the details described above, for example, you can just call a function like readRangeResult() which does the I2C work for you.

The libraries for this sensor are typical of this style of library. For example, the Adafruit_VL53L0X library’s startRange() command sends a write command to start a reading of the sensor. The isRangeComplete() sends a read command to read the register that indicates whether or not the reading is done. 

All sensors take a certain amount of time to read the physical phenomena which they sense. With light-based sensors like the VL53L0X, the time they take to get a reading is usually called ranging time, or ranging measurement time, and it’s noted in the data sheet how long it is (about 23ms). In operation, you query the sensor as to whether it’s got a reading available, and then read it when that’s true.

How To Pick a Library

Most companies that make breakout boards for a given I2C sensor will also write a library for it. A search on the term “VL53L0X” in the Arduino library manager will return several libraries. All of them will work with any of the breakout boards. What’s the difference? Ideally, you want a library that works well, has readable examples, and good documentation. Often you have to settle for two of the three.

Different companies and programmers have different styles for writing a library’s application programming interface, or API. Arduino has a style guide for writing APIs, but it’s not always followed by others. The Arduino-developed libraries generally follow this guide. Other companies’ libraries may have a few more configuration functions, and their examples are a bit more complicated as a result. You should look at the examples with any library to see if they make sense to you. A good guideline is to use the library with the instructions and examples that you find to be the clearest. Here is the Adafruit guide for this sensor. As you can see, it doesn’t tell you much about the functions themselves. However, you can also get information from the library’s header file. The header file is generally the file with the name libraryname.h. Sometimes it’s in a directory called src. For example, here’s the header file for the Adafruit_VL53L0X library. Within the header file, there are class names for the library and a section called public where you’ll find all the possible function definitions.

Even if the examples don’t include all the functions, the public section of the header file will. From there, you can build your own examples if the library’s examples don’t show how to use a function you want to use.

Adafruit’s library for this sensor is quite complex, and includes two different ways of reading the sensor. In their examples, they use a function called rangingTest(), which puts data into an object in memory called measure in the examples, and then you pull the readings from that object. However, you can also read the results directly using a set of simpler functions:

  • startRange() – starts the sensor taking a single reading
  • readRange() – returns a single range reading
  • readRangeResult() – returns a single range reading, and an error value (oxFFFF) if there’s an error
  • startRangeContinuous() – starts the sensor reading continuously
  • isRangeComplete() – returns true when a reading is complete and ready to be read
  • waitRangeComplete() – does nothing until the reading is complete

Pololu’s library is similar, but the API is a bit simpler, and their header file lists all the register addresses, for those interested in the lower-level details. They document all the functions on their repository’s main page as well. Their product documentation is clear, if shorter than Adafruit’s.

Install the External Libraries

You can use the library manager to find these libraries. Make sure you’re using Arduino version 1.8.14 or later. From the Sketch menu, choose Include Library, then Manage Libraries, and search for VL53L0X. All of the related libraries mentioned here will show up. The examples below use the Adafruit_Vl53L0X library.

Program the Microcontroller

At the beginning of your code, include the appropriate libraries. In the setup(), initialize the sensor with a function called begin() (Sparkfun sometimes uses init() instead of begin()). If the sensor responds, then begin() will return true; if not, it will return false. This is how to check that the sensor is properly wired to your microcontroller, and to configure it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// include library
#include "Adafruit_VL53L0X.h"
 
// make an instance of the library:
Adafruit_VL53L0X sensor = Adafruit_VL53L0X();
 
const int maxDistance  = 2000;
 
void setup() {
  // initialize serial, wait 3 seconds for
  // Serial Monitor to open:
  Serial.begin(9600);
  if (!Serial) delay(3000);
 
  // initialize sensor, stop if it fails:
  if (!sensor.begin()) {
    Serial.println("Sensor not responding. Check wiring.");
    while (true);
  }
  /* config can be:
    VL53L0X_SENSE_DEFAULT: about 500mm range
    VL53L0X_SENSE_LONG_RANGE: about 2000mm range
    VL53L0X_SENSE_HIGH_SPEED: about 500mm range
    VL53L0X_SENSE_HIGH_ACCURACY: about 400mm range, 1mm accuracy
  */
  sensor.configSensor(Adafruit_VL53L0X::VL53L0X_SENSE_LONG_RANGE);
  // set sensor to range continuously:
  sensor.startRangeContinuous();
}

In the main loop() function, you’ll read the sensors. You’re going to query the sensor to see if it’s got a reading available with the isRangeComplete() function, then you’ll use the readRangeResult() function to get the result. This function returns a result in millimeters:

1
2
3
4
5
6
7
8
9
10
11
12
13
void loop() {
  // if the reading is done:
  if (sensor.isRangeComplete()) {
    // read the result:
    int result = sensor.readRangeResult();
    // if it's with the max distance:
    if (result < maxDistance) {
      // print the result (distance in mm):
      Serial.println(result);
 
    }
  }
}

Run this sketch now, and it will print out the distance from the sensor in millimeters (link to the full sketch).

Conclusion

I2C is a common protocol among many ICs, and it’s handy because you can combine many devices on the same bus. When doing so, however, make sure the device addresses are unique. This can complicate things if you want to use multiple sensors of the same type on the same I2C bus. Fortunately, this is not often the case.

I2C can also be used to combine several Arduinos on a bus, with one as the controller and the others as peripherals. If you build your own Arduino-compatible circuit on a breadboard, this can be an inexpensive way to combine several controllers in a more complex project. There are examples of this in the Wire library documentation on the Arduino site.

Lab: I2C Communication With a Color, Gesture, and Proximity sensor

In this lab, you’ll see synchronous serial communication in action using the Inter-integrated Circuit (I2C) protocol. You’ll communicate with a color, gesture, and proximity sensor from a microcontroller.

Introduction

In this lab, you’ll see synchronous serial communication in action using the Inter-integrated Circuit (I2C) protocol. You’ll communicate with a color, gesture, and proximity sensor from a microcontroller.

There are many different sensors on the market that use the I2C protocol to communicate with microcontrollers. It is the most common way to connect to sensors these days. The one used in this lab is typical, so the I2C principles covered here will help you when working with other I2C sensors as well.

Related videos: Intro to Synchronous Serial, I2C

What You’ll Need to Know

To get the most out of this Lab, you should be familiar with the basics of programming an Arduino microcontroller. If you’re not, review the Digital Input and Output Lab, and perhaps the Getting Started with Arduino guide. You should also understand asynchronous serial communication and how it differs from synchronous serial communication.

Things You’ll Need

Figure 1-3 are the parts that  you need for this lab.

Three 22AWG solid core hookup wires. Each is about 6cm long. The top one is black; the middle one is red; the bottom one is blue. All three have stripped ends, approximately 4 to 5mm on each end.
Figure 1. 22AWG solid core hookup wires.
Photo of an Arduino Nano 33 IoT module. The USB connector is at the top of the image, and the physical pins are numbered in a U-shape from top left to bottom left, then from bottom right to top right.
Figure 2. Arduino Nano 33 IoT or other Arduino board
Photo of an APDS-9960 color and gesture sensor breakout board.
Figure 3. An APDS-9960 color and gesture sensor breakout board.

Sensor Characteristics

The sensor used in this lab,  a Broadcom APDS-9960 sensor, is an integrated circuit (IC) that can read the color of an object placed in front of it; proximity, within about 10cm; and gestures on the axes parallel to the sensor (up, down, left, and right). It senses color using four photodiodes, three of which have color filters (red, green, and blue) and one of which has no filter (clear). The color sensors are filtered to block IR and UV light. It senses gesture using four directional photodiodes, picking up reflected IR energy from a built-in IR LED. The combination of sensors is used to determine the direction of an object moving above the board, and its proximity.

The company that makes this sensor, Broadcom, doesn’t make their own breakout boards, but a few other companies do. There’s a breakout board available from Sparkfun and one from Adafruit and one from DFRobot (all available through Digikey) and there’s an APDS9960 sensor built into the Arduino Nano 33 BLE Sense board as well.

I2C Connections

Connect the sensor’s voltage and ground connections to your voltage and ground buses, and the connections for I2C clock (SCL) and I2C serial data (SDA) as shown in Figure 4-6. The schematic, Figure 4, is the same for both the Uno and the Nano. For the Arduino Uno or the Arduino Nano boards, the I2C pins are pins A4 (SDA) and A5(SCL). This is the same connection for almost any I2C sensor.

Some I2C sensors also have a few other pins:

  • an interrupt pin, which they use to signal the microprocessor when a reading is ready. The APDS-9960 has an interrupt pin, but you don’t have to use it if you don’t want to. Your code will need to change if you use the interrupt. You can read more about that later in this lab.
  • a shutdown or reset pin, which can be used for powering down or resetting the sensor. This sensor doesn’t have that pin.

What are Qwiic/Stemma/Grove/Gravity?

In addition to the standard I2C connections, Sparkfun and Adafruit use a connector called Qwiic which connects the I2C, power, and interrupt connectors all in one cable, eliminating the need for soldering. It’s a Sparkfun brand name. However, you’ll need a Qwiic adapter shield to use it. Adafruit have a similar brand called Stemma, Seedstudio uses Grove, and DFRobot uses Gravity. They all support I2C, and they all have custom solderless connectors, though they are not all compatible with each other. The most compatible way is to stick with the I2C header pins.

Schematic view of an Arduino attached to an APDS-9960 sensor.
Figure 4. Schematic view of an Arduino attached to an APDS-9960 sensor.
Breadboard view of an Arduino attached to an APDS-9960 sensor
Figure 5. Breadboard view of an Arduino attached to an APDS-9960 sensor. This shows a Sparkfun breakout board, but the other companies’ boards use the same pins: voltage, ground, SDA, and SCL.
An APDS-9960 color sensor breakout board connected to an Arduino Nano 33 IoT
Figure 6. An APDS-9960 color sensor breakout board connected to an Arduino Nano 33 IoT. This shows a Sparkfun breakout board, but the other companies’ boards use the same pins: voltage, ground, SDA, and SCL.

The circuit is now complete, and you’re ready to write a program to control it. One of the advantages of the I2C synchronous serial protocol (as opposed to the SPI protocol) is that you only ever need two wires for communication to one or multiple devices.

How I2C Sensors Work

I2C devices exchange bits of data whenever the shared clock signal changes.  Controller and peripheral devices both send bits of data when the clock changes from low to high (called the rising edge of the clock). Unlike with SPI, they cannot send data at the same time.

The APDS-9960 has a series of memory registers that control its function. You can write to or read from these registers using I2C communication from your microcontroller. Some of these registers are writable by the controller so that you can configure the sensor. For example, you can set set the sensitivity of the sensor, and so forth. Some registers are configuration registers, and by writing to them, you configure the chip. For example, you can set lower and upper limits of temperature sensitivity. Other memory registers are read-only. For example, when the sensor has read the proximity of an object, it will store the result in a register that you can read from the controller. The details of the chip’s registers can be found in the sensor’s datasheet.

How I2C Bits are Exchanged

Most of the time, you never have to think about how the bits of an I2C message are exchanged, and the next section, I2C Libraries, will be more important to you. For the low-level details, read on:

I2C devices exchange data in 7-bit chunks, using an eighth bit to signal if you’re reading or writing by the controller or for acknowledgement of data received. To get the temperature from the APDS-9960, your controller device sends the sensor’s address (a 7-bit number, for this sensor it’s 0×39) followed by a single bit indicating whether you want to read data or write data (1 for read, 0 for write). This means that the 8-bit byte sent is actually 0x72 or 0x73, depending on the state of the read/write bit. Then you send the memory register that you want to read from or write to. For example, as shown in Figure 7, the proximity reading is stored in memory register 0x9C of the APDS9960. To get the proximity, you send 0x72 (0x39 shifted up one bit, with 0 in the R/W bit); then 0x9C for the register you want to read. The response in this case is 0x77. The bottom bit is a 1, meaning no ACK was sent from the sensor. That converts to a proximity reading of 118.

Figure 7. I2C data

I2C and the Wire Library

To use I2C communication on an Arduino microcontroller, you use the Wire library, which is built into the Arduino IDE. You can find Arduino-compatible libraries for many devices that use the Wire library, but never expose it directly in their APIs. The libraries for this sensor are typical of this style of library. For example, the Arduino_APDS9960 library’s readColor() command sends a write command to start a reading of the sensor’s color photodiodes. The colorAvailable() sends a read command to read the register that indicates whether or not the color reading is done. 

All sensors take a certain amount of time to read the physical phenomena which they sense. With light-based sensors like the APDS-9960, the time they take to get a reading is usually called integration time, and it’s noted in the data sheet how long it is (page 4). The color sensor of the APDS-9960 has an integration time of between 2.78ms and 708ms, depending on your settings. In operation, you query the sensor as to whether it’s got a reading available, and then read it when that’s true.

How To Pick a Library

Most every company that makes a breakout board for a given I2C sensor will also write a library for it. For example, there’s the Arduino_APDS9960, the SparkFun_APDS9960, and the Adafruit_APDS9960 library. All three of these will work with any of the three breakout boards. The Arduino Nano 33 BLE sense will only work with the Arduino_APDS9960 library. Otherwise, what’s the difference?

Different companies and programmers have different styles for writing a library’s application programming interface, or API. Arduino has a style guide for writing APIs, but it’s not always followed by others. The Arduino_APDS9960 library follows this guide, and has the simplest API of the three. The Sparkfun_APDS9960 offers a few more configuration functions, as does the Adafruit_APDS9960, and their examples are a bit more complicated as a result. You should look at the examples with any library to see if they make sense to you. A good guideline is to use the library with the instructions and examples that you find to be the clearest. It’s also good to check the company’s guide to the sensor if they have one. Here are the guides for this sensor: Sparkfun Hookup guide; Adafruit guide; Arduino library reference.

You can also get information from the library’s header file. The header file is generally the file with the name libraryname.h. Sometimes it’s in a directory called src. For example, here’s the header file for the Arduino_APDS9960 library. Here’s the one for the Sparkfun library and the one for the Adafruit library. Within the header file, there’s a class names for the library and a section called public where all the possible function definitions are.

Even if the examples don’t include all the functions, the public section of the header file will. From there, you can build your own examples if the library’s examples don’t show how to use a function you want to use.

All three libraries operate in more or less the same way, because they have to access the same functions of the sensor. They start the function (color, proximity, or gesture) using an enable function in the setup. In the main loop, they query the sensor if it’s got a reading, and then read it if it does. For example, to use the color function, the Arduino and Adafruit libraries have functions that check if the sensor’s got a good reading: colorDataReady() in the Adafruit library and colorAvailable() in the Arduino library. The Sparkfun has no function like this, so you have to add a delay between color readings.

The Sparkfun and Adafruit libraries provide functions to explicitly enable or disable the sensor’s three major functions. The Arduino library does this work implicitly by enabling each function when you call the available() functions, and disabling the function after each read. The former give you more control, but require you to make sure you’ve done the enabling and disabling. The latter is more automatic, but gives you less discrete control.

Install the External Libraries

You can use the library manager to find these libraries. Make sure you’re using Arduino version 1.8.9 or later. From the Sketch menu, choose Include Library, then Manage Libraries, and search for APDS9960. All three of the libraries mentioned here will show up. The examples below use the Arduino_APDS9960 library, as it’s the simplest of the three.

Program the Microcontroller

At the beginning of your code, include the appropriate libraries. In the setup(), initialize the sensor with a function called begin() (Sparkfun sometimes uses init() instead of begin()). If the sensor responds, then begin() will return true, and if not, it will return false. This is how to check that the sensor is properly wired to your microcontroller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "Arduino_APDS9960.h"
 
void setup() {
  Serial.begin(9600);
  // wait for Serial Monitor to open:
  while (!Serial);
  // if the sensor doesn't initialize, let the user know:
  if (!APDS.begin()) {
    Serial.println("APDS9960 sensor not working. Check your wiring.");
    // stop the sketch:
    while (true);
  }
 
  Serial.println("Sensor is working");
}

In the main loop() function, you’ll read the sensors. Here’s how to read the color sensor using the Arduino_APDS9960 library. You’re going to query the sensor to see if it’s got a reading available with the available() function, then you’ll use the read() function to get the result:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void loop() {
  // red, green, blue, clear channels:
  int r, g, b, c;
 
  // if the sensor has a reading:
  if (APDS.colorAvailable()) {
 
    // read the color
    APDS.readColor(r, g, b, c);
 
    // print the values
    Serial.print(r);
    Serial.print(",");
    Serial.print(g);
    Serial.print(",");
    Serial.print(b);
    Serial.print(",");
    Serial.println(c);
  }
}

You can run this sketch now, and it will print out values for red, green, blue, and clear channels, like so (Here’s a link to the full sketch):

Sensor is working
17,12,16,40
17,12,16,41
17,12,16,41
17,12,16,41
17,12,16,41
17,12,16,40
16,12,16,40
16,11,15,39

As you can see, you’re never actually calling commands from the Wire library directly, but the commands in the sensor’s library are relying on the Wire library. This is a common way to use the Wire library to handle I2C communication. If you’re looking for more examples with this sensor, the libraries all come with examples when you install them. There are some other examples for this sensor in the github repository for this class, using all three libraries. There are two important things to note:

  • None of the three libraries give light readings in lux, or proximity readings in millimeters.
  • All of the libraries are controlling the same sensor, and therefore can yield the same results. You may need to configure the sensor differently for each library though. Check the header files for each library (Arduino, Adafruit, Sparkfun) to learn their configuration functions, and check the data sheet to learn more about the sensor’s characteristics.

Using the Sensor’s Interrupt

Most I2C sensors include an interrupt pin. This pin can be used to signal the microcontroller when something important happens, like when the sensor has a good reading, or when the sensor reading crosses a particular threshold. Using the interrupt means that as soon as the sensor is ready to give you a reading, it interrupts the microcontoller.

The interrupt pin for an I2C sensor is usually configurable through the sensor’s API. For example, with the Sparkfun and Adafruit libraries for this sensor you can set the interrupt to signal when any of the three functions changes significantly, and with the Sparkfun library you can set the low and high thresholds for the proximity function.

For basic use of most sensors, you don’t need the interrupt, for for advanced use, it can be helpful. For more on using interrupts, see the Arduino reference page on interrupts.

Conclusion

I2C is a common protocol among many ICs, and it’s handy because you can combine many devices on the same bus. You need to make sure the device addresses are unique. Some devices will have fixed addresses, so that you can’t use multiples of the same sensor together. Others will have a way to change the address.  For the APDS9960, the address is fixed.

I2C can also be used to combine several Arduinos on a bus, with one as the controller and the others as peripherals. If you build your own Arduino-compatible circuit on a breadboard, this can be an inexpensive way to combine several controllers in a more complex project. There are examples this in the Wire library documentation on the Arduino site.

Lab: I2C Communication With An Infrared Temperature Sensor

In this lab, you’ll see synchronous serial communication in action using the Inter-integrated Circuit (I2C) protocol. You’ll communicate with an infrared temperature sensor chip from a microcontroller in order to read the temperature of an object in front of the sensor.

Introduction

In this lab, you’ll see synchronous serial communication in action using the Inter-integrated Circuit (I2C) protocol. You’ll communicate with an infrared temperature sensor chip from a microcontroller in order to read the temperature of an object in front of the sensor.

Related videos: Intro to Synchronous Serial, I2C

What You’ll Need to Know

To get the most out of this Lab, you should be familiar with the basics of programming an Arduino microcontroller. If you’re not, review the Digital Input and Output Lab, and perhaps the Getting Started with Arduino guide. You should also understand asynchronous serial communication and how it differs from synchronous serial communication.

Things You’ll Need

Figure 1-3 are the parts that  you need for this lab.

Three 22AWG solid core hookup wires. Each is about 6cm long. The top one is black; the middle one is red; the bottom one is blue. All three have stripped ends, approximately 4 to 5mm on each end.
Figure 1. 22AWG solid core hookup wires.
Photo of an Arduino Nano 33 IoT module. The USB connector is at the top of the image, and the physical pins are numbered in a U-shape from top left to bottom left, then from bottom right to top right.
Figure 2. Arduino Nano 33 IoT
A TMP007 Temperature sensor module. There are 7 pins along the bottom, labeled VCC, GND, SDA, SCL, Alert, AD1, and AD0 (left to right)
Figure 3. A TMP007 Temperature sensor module.
A photo of a short solderless breadboard with two rows of holes along each side. There are no components mounted on the board. The board is oriented sideways so that the long rows of holes are on the top and bottom of the image.
Figure 4. A short solderless breadboard.

Connect the temperature sensor

The temperature sensor used in this lab,  a Texas Instruments TMP007, is an integrated circuit (IC) that can read the temperature of an object placed in front of it. Connect the sensor’s power and ground connections and the connections for clock and serial data as shown in Figure 5-7. For the Arduino UNO board and Nano boards, the I2C pins are fixed A4(SDA) and A5(SCL):

Schematic view of an Arduino attached to a TMP007 temperature sensor. The TMP007 temperature sensor has 7 pins, and when the sensor is positioned with the pins on the bottom and pointing away from you, the pins are labeled VCC, Ground, SDA, SCL, Alert, AD1, and AD0. The sensor's VCC and ground pins (pins 1 and 2) are connected to the Aruino's 5V and GND pins, respectively. The SCL pin (pin 4) is connected to the Arduino's A4 input and the Alert pin (pin 5) is connected to the A5 input.
Figure 4. Schematic view of an Arduino attached to a TMP007 temperature sensor.
Schematic view of an Arduino attached to a TMP007 temperature sensor. The TMP007 temperature sensor has 7 pins, and when the sensor is positioned with the pins on the bottom and pointing away from you, the pins are labeled VCC, Ground, SDA, SCL, Alert, AD1, and AD0. The sensor's VCC and ground pins (pins 1 and 2) are connected to the Aruino's 5V and GND pins, respectively. The SCL pin (pin 4) is connected to the Arduino's A4 input and the Alert pin (pin 5) is connected to the A5 input.
Figure 5. Schematic view of an Arduino attached to a TMP007 temperature sensor.
An Arduino Nano attached to a TMP007 temperature sensor. The TMP007 temperature sensor has 7 pins, and when the sensor is positioned with the pins on the left hand side of the board the pins are labeled VCC, Ground, SDA, SCL, Alert, AD1, and AD0. The sensor's VCC and ground pins (pins 1 and 2) are connected to the Arduino's 3.3V (pin 2) and GND (pin 14) pins, respectively. The sensor's SDA pin (pin 3) is connected to the Arduino's A4 input (pin 8) and the sensor's SCL pin (pin 4) is connected to the Arduino's A5 input (pin 9).
Figure 6. An Arduino Nano attached to a TMP007 temperature sensor. The TMP007 temperature sensor has 7 pins, and when the sensor is positioned with the pins on the left hand side of the board the pins are labeled VCC, Ground, SDA, SCL, Alert, AD1, and AD0. The sensor’s VCC and ground pins (pins 1 and 2) are connected to the Arduino’s 3.3V (pin 2) and GND (pin 14) pins, respectively. The sensor’s SDA pin (pin 3) is connected to the Arduino’s A4 input (pin 8) and the sensor’s SCL pin (pin 4) is connected to the Arduino’s A5 input (pin 9).

The circuit is now complete, and you’re ready to write a program to control it. One of the advantages of the I2C synchronous serial protocol (as opposed to the SPI protocol) is that you only ever need two wires for communication to one or multiple devices.

How the Temperature Sensor Works

I2C devices exchange bits of data whenever the shared clock signal changes.  Controller and peripheral devices both send bits of data when the clock changes from low to high (called the rising edge of the clock). Unlike with SPI, they cannot send data at the same time.

The TMP007 has a series of memory registers that control its function. You can write to or read from these registers using I2C communication from your microcontroller. Some of these registers are writable by the controller so that you can configure the sensor. For example, you can set its I2C address, set the sensitivity of the sensor, and so forth. Some registers are configuration registers, and by writing to them, you configure the chip. For example, you can set lower and upper limits of temperature sensitivity. Other memory registers are read-only. For example, when the sensor has read the temperature of an object, it will store the result in a register that you can read from the controller. The details of the chip’s registers can be found in the TMP007 datasheet, in the “Register Maps” section, page 26.

I2C devices exchange data in 7-bit chunks, using an eighth bit to signal if you’re reading or writing by the controller or for acknowledgement of data received. To get the temperature from the TMP007, your controller device sends the sensor’s address (a 7-bit number) followed by a single bit indicating whether you want to read data or write data (1 for read, 0 for write). Then you send the memory register that you want to read from or write to. For example, as shown in Figure 7, the object temperature is stored in memory register 01 of the TMP007. To get the temperature, you send:

I2C data. I2C communication. The first 3 bytes are messages from the controller to the peripheral. The first byte is a one followed by 7 zeros. The first 7 bits in the byte are for the peripheral at address 0x40 and the last bit indicates this is a write command. The second byte is 6 zeros, a one, and a zero. The first 7 bits in the byte indicates this is to read register 0x01 and the last bit is an acknowledgement from the device. The third byte is a one, 6 zeros, and a one. The first 7 bytes again are for the peripheral at address 0x40 and the last bit indicates this is a read command. The final 2 bytes are messages from the peripheral to the controller. The first byte is for 6 zero bits, a one, and a zero. The first 7 bits are the data in register 0x01 and the last bit is an acknowledgement from the device. The second byte is 6 zero bits and 2 ones. The first 7 bits are again data in the register 0x01 and the last bit is a signal from the device that the message is complete.
Figure 7. I2C data

To use I2C communication on an Arduino microcontroller, you use the Wire library, which is built into the Arduino IDE. You can find Arduino-compatible libraries for many devices that use the Wire library, but never expose it directly in their APIs. Adafruit’s TMP007 library is typical of this style of library. The library’s readObjTempC() command sends a write command to start a temperature reading of the object in front of the sensor, then sends a read command to read the appropriate temperature register, then combines the results using a formula explained in the datasheet on page 25, then returns the result to you as degrees celsius. Similarly, the readDieTempC() sends a write command to read the chip temperature, then a read command to read the appropriate temperature register, then does the math and gives you the result.

Install the External Libraries

The TMP007 library relies on the Adafruit Sensor library in addition to the Wire library, so you’ll need both in order to make this work. You can use the library manager to find these libraries or download the TMP007 library here and the Sensor library here. Make sure you’re using Arduino version 1.5.5 or later.

Once you’ve downloaded the libraries, change the name of the resulting .zip file to remove the words -master or _Library-master from the end of the file name. Keep the .zip extension though. Open the Arduino IDE (version 1.6.8 or later) and choose the Sketch menu, then choose Import Library… and finally click the Add Library… option. A dialog box will pop up asking you to find the library you want to add. Navigate to the .zip file for the Sensor library and choose it. The library will be added and you’ll see the words “Library added to your libraries. Check the ‘Import Library’ menu”.

Program the Microcontroller

At the beginning of your code, include the appropriate libraries. In the setup(), initialize the sensor with begin(). If the sensor responds, then begin() will return true, and if not, it will return false. This is how to check that the sensor is properly wired to your microcontroller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "Wire.h"
#include "Adafruit_TMP007.h"
 
Adafruit_TMP007 tmp007; // instance of the sensor library
 
void setup() {
  Serial.begin(9600);
  boolean sensorInitialized = tmp007.begin(); // initialize the sensor
 
  while (!sensorInitialized) {
    // Do nothing until sensor responds
    Serial.println("Sensor is not responding");
  }
}

The TMP007 can only read every four seconds. In the main loop, set up an if statement to check when the millis() passes another four seconds using the modulo operator:

1
2
3
4
void loop() {
  if (millis() % 4000 < 2) { // if 4 seconds have passed
  }
}

Inside that if statement, read the object temperature and the chip temperature and print them both out:

1
2
3
4
5
6
7
8
9
10
11
12
13
void loop() {
  if (millis() % 4000 < 2) { // if 4 seconds have passed
    float objectTemp = tmp007.readObjTempC(); // read object temperature
    float chipTemp = tmp007.readDieTempC(); // read chip temperature
 
    // print the results:
    Serial.print("Chip temperature: ");
    Serial.print(chipTemp);
    Serial.print(" deg. C. \t Object temperature: ");
    Serial.print(objectTemp);
    Serial.println(" deg. C");
  }
}

As you can see, you’re never actually calling commands from the Wire library directly, but the commands in the TMP007 library are relying on the Wire library. This is a common way to use the Wire library to handle I2C communication.

Conclusion

I2C is a common protocol among many ICs, and it’s handy because you can combine many devices on the same bus. You need to make sure the device addresses are unique. Each device will have its own way to change the address.  For the TMP007, see Table 2 in the data sheet; you have to set the address pins (AD1 and AD0 on the breakout board appropriately to change the address.

I2C can also be used to combine several Arduinos on a bus, with one as the controller and the others as peripherals. If you build your own Arduino-compatible circuit on a breadboard or use boards like the Adafruit Trinket or Trinket Pro, this can be an inexpensive way to combine several controllers in a more complex project. There are examples this in the Wire library documentation on the Arduino site.

Synchronous Serial Communication: The Basics

Introduction

Related Video: Intro to Synchronous Serial

Asynchronous serial communication, which you can see in action in the Serial Output lab, is a common way for two computers to communicate. Both computers must have their own clock, and keep time independently of each other. This works well for personal computers, mobile devices, and microcontrollers because they all have their own clock crystal that acts as the processor’s heartbeat. However, there are simpler integrated circuits that have only one function, such as to read a sensor or to control a digital potentiometer or an oscillator to drive a PWM circuit. These ICs have no clock crystal of their own. They consist of a few memory registers and the minimal circuitry needed to connect these memory registers to the circuit that they control. To communicate with these ICs, you need to use synchronous serial communication.

To get the most out of these notes, you should know what a microcontroller is and have an understanding of the basics of microcontroller programming. You should also understand the Asynchronous Serial Communication: The Basics as well.

Synchronous serial communication protocols feature a controller device which sends a clock pulse to one or more peripheral devices. The devices exchange a bit of data every time the clock changes. There are two common forms of synchronous serial, Inter-Integrated Circuit, or I2C (sometimes also called Two-Wire Interface, or TWI), and Serial Peripheral Interface, or SPI.

Synchronous serial devices communicate by shifting bits of data along their communication lines, like a bucket brigade. Data moved down the line one bit every time the clock pulses. All the devices in a synchronous serial chain share the same data and clock lines. Peripheral devices are directed by the controller device when to listen to the bits coming down the line, and when to ignore them. However, the two most common synchronous serial protocols, SPI and I2C, use different methods for directing the peripheral devices.

Serial Peripheral Interface (SPI)

Related video: SPI

Related Lab: SPI Communication With A Digital Potentiometer

SPI devices are connected by four wires, as shown in Figure 1:

  • a Serial Data In (SDI), on which the controller sends data to the peripheral devices.
  • a Serial Data Out (SDO), on which the peripheral devices send data to the controller.
  • a Clock  (SCLK) connection, on which the controller sends a regular clock signal to the peripheral devices.
  • one or more Chip Select (CS)  connections, which the controller uses to signal the peripheral devices when to listen to incoming data and when to ignore it.

A Note on Pin Naming

The electronics industry has used the terms “master/slave” to refer to controller devices and peripheral devices for decades without regard for the historical context of, and offense caused by, those terms. As a result, you will see the terms MOSI/MISO/SS  in data sheets to refer to the pins of an SPI device.  While a modern standard naming scheme has not yet emerged to replace these, the Open Source Hardware Association has a proposal on the table. Make Magazine proposes retaining the acronym while renaming the terms. The SDO, SDI and CS terms are currently used by a handful of companies within the industry, but have some ambiguity when used in practice. Hence, the PICO/POCI proposal. The debate is not resolved, and you will likely see some variations on the terms. The SDO, SDI, and SCK terms are the most widely accepted terms that do not carry historical baggage.

The SDI, SDO, and SCLK connections are shared between all the devices connected to the controller. This configuration is called a bus. Each peripheral has its own dedicated Chip Select connection to the controller, however.

Diagram of SPI synchronous serial communication between a microcontroller and three peripheral devices. There are three lines which connect the microcontroller to all three peripherals. They are labeled SDI (serial data in to controller), SDO (serial data out from controller), and SCLK. There are an additional three lines, each going directly from the microcontroller to each peripheral, labeled CS1 (chip select 1), CS2, and CS3.

Figure 1. A typical SPI bus configuration. The Controller’s output (SDO) is the peripherals’ input (SDI) and vice versa. Each peripheral gets its own Chip Select line. All other lines are shared.

When the controller device wants to communicate with one of the peripherals, it sets that device’s Chip Select pin low. The peripheral will then listen for new bits of data on the microcontroller’s Serial Data Out (SDO) line every time the clock changes from low to high (called the rising edge of the clock). If it is instructed to send any data back, it will send data back to the controller when the clock signal changes from high to low (called the falling edge of the clock). When a peripheral device’s Chip Select pin is high, it will not respond to any commands sent on the data line.

The data exchange between SPI devices is usually shown like this (Figure 2):

This diagram shows a graph of the changes in the SDI, SDO, and SCK lines between a microcontroller and its peripheral during SPI synchronous serial communication. It shows that data is written out from the microcontroller to the peripheral when the clock line changes from low to high. data is read in from the peripheral when the clock line changes from high to low.
Figure 2. Timing diagram for SPI serial communication

The Arduino’s SPI pins are determined by the processor. You can find the pins for the various models on the SPI library reference page. For the Arduino Uno, the pin numbers are pin 11 for SDO, pin 12 for SDI, and pin 13 for Clock (SCK). Pin 10 is the default Chip Select pin (SS), but you can use other pins for Chip Select as needed. The Arduino SPI library allows you to control the SPI bus. Most SPI devices that are compatible with Arduino come with their own libraries, however, which wrap the SPI library in commands specific to the device in question.

For example, the Analog Devices ADXL345 accelerometer can communicate via SPI. Its protocol works as follows: first the controller sets the ADXL345’s Chip Select (CS) pin low, then sends a command to the ADXL345 on the SDI line to enter measurement mode. The ADXL345 then continually samples the accelerometer and stores the latest readings in three memory registers. When the controller wants to know those values, it sets the Chip Select (CS) pin low and sends a request to read those memory registers. The ADXL345 responds by sending back the contents of the memory registers on the SDO line. When all the data has been received, the controller sets the Chip Select pin high again.

The advantage of SPI is that the data transactions are simple: all you need to do is to send the data to the device you’re communicating with. The disadvantage is that the number of wires needed to connect goes up by one for every peripheral device you add to the bus.

Screenshot of data capture from a microcontroller communicating with an Analog Devices digital Potentiometer over SPI. The image shows the change in voltage over time of the SPI connections between a microcontroller and the peripheral device. The potentiometer sends no data, but the controller sends two bytes over the MOSI line.
Figure 3. Data capture from a microcontroller communicating with an Analog Devices digital Potentiometer over SPI. The potentiometer sends no data, but the controller sends two bytes over the SDO line.

Inter-Integrated Circuit (I2C) or Two-Wire Interface (TWI)

Related video: I2C

Related Lab: I2C Communication With An Infrared Temperature Sensor

I2C is another popular synchronous serial protocol. It also uses a bus configuration like SPI, but there are only two connections between the controller device and the peripheral devices as shown in Figure 4:

  • a Serial Clock (SCL) connection, on which the controller sends the clock signal, just as in SPI
  • a Serial Data (SDA) connection, on which the controller and peripherals exchange data in both directions.
Diagram of I2C synchronous serial communication. There are two wires connecting the microcontroller and the three peripheral devices, labeled SDA (serial data) and SCL (serial clock). The same two lines connect all three peripheral devices.
Figure 4. Diagram of I2C synchronous serial communication.

Each I2C peripheral device has a unique address on the bus. When the controller wants to communicate with a particular peripheral, it sends that peripheral’s address down the SDA connection, transferring each bit on the rising edge of the clock. An extra bit indicates whether the controller wants to write or read to the peripheral that it’s addressing. The appropriate peripheral then goes into listening mode, and the controller sends a command to the peripheral. Then the controller switches its connection to the SDA line from output to input. The peripheral then replies with the appropriate data, sending each bit on the falling edge of the clock. The controller switches its connection on the SDA line back to output once it’s received all of the data.

The I2C data capture in Figure 5 is typical (click to enlarge it). This is from a Texas Instruments TMP007 temperature sensor. The peripheral’s address is 0x40. First the controller sends a byte with 0x40 + 0 in the final bit, indicating that it plans to write a command to the peripheral. All of this data is sent valid on the rising edge of the clock. Then the controller sends a command, 0x03, which means “tell me your object’s temperature” to this particular IC. Then the controller sends a byte with the peripheral’s address again, 0x40 +1 in the final bit, indicating that it wants to read from the peripheral. The peripheral responds with two bytes, 0x0B and 0xC0. The controller then puts those two bytes together to get the object’s temperature (see the TMP007 datasheet if you want to know more)

Data capture from a microcontroller communicating with an TMP007 temperature sensor using I2C communication. The image shows the change in voltage over time of the SPI connections between a microcontroller and the peripheral device. The direction of data transfer is not discernable from the electrical signals alone, so you have to rely on the value of the bytes they represent to know what is going on.
Figure 5. Data capture from a microcontroller communicating with an TMP007 temperature sensor using I2C communication. he advantage of I2C is that you really only need two wires to connect all the I2C devices you want to your controller. The disadvantage is that you have to send an address before you send any command.

The Arduino’s I2C pins are determined by the processor. You can find the pins for the various models on the Wire library reference page. The Arduino Wire library allows you to control the I2C bus. For the Arduino Uno, the pin numbers are analog pin 4 for SDA and analog pin 5 for SCL. On the Uno rev.3 layout, SDA and SCL are also broken out on the digital side of the board, next to the ground pin.  Most I2C devices that are compatible with Arduino come with their own libraries which wrap the Wire library in commands specific to the device in question. For example, Adafruit‘s library for the TMP007 relies on the Wire library to transmit and receive data.

I2C Control of Multiple Microcontrollers

You can also use I2C as a way to control many microcontrollers from one central controller. For example, if you needed to operate a large number of servomotors, you could put five or six each on a single Arduino, then chain several Arduinos together in an I2C chain and program them all to respond as peripherals. Then you would program a central microcontroller as the controller, and have it send commands to the peripheral devices when it’s time to move each device’s servos. You can see an example of how to do this in this example from the Arduino site.

What are Qwiic/Stemma/Grove/Gravity?

In addition to the standard I2C connections, Sparkfun and Adafruit use a connector called Qwiic which connects the I2C, power, and interrupt connectors all in one cable, eliminating the need for soldering. It’s a Sparkfun brand name. However, you’ll need a Qwiic adapter shield to use it. Adafruit have a similar brand called Stemma, Seedstudio uses Grove, and DFRobot uses Gravity. They all support I2C, and they all have custom solderless connectors, though they are not all compatible with each other. The most compatible way is to stick with the I2C header pins.

Conclusion

SPI and I2C are useful protocols because they allow you to interface with a wide variety of sensor and actuator ICs without having to use many of your microcontroller’s IO pins. Because they are both bus protocols, you can chain many devices on the same bus, and call on them only when needed from your microcontroller. For more on their usage, see the Lab: SPI Communication With A Digital Potentiometer and the Lab: I2C Communication With An Infrared Temperature Sensor.