Blues University is an ongoing series of articles geared towards beginner- and intermediate-level developers who are looking to expand their knowledge of embedded development and the IoT.
The world of the IoT is filled with a wide array of sensors. To transfer data between computing resources, devices need an agreed-upon communication standard, or protocol, for proper communication. Today four standards have come to dominate the world of sensor communication: UART, I2C, SPI, and CAN.
Each of these protocols have different characteristics, strengths, and weaknesses, often making one more appropriate than others. This guide will show you how each is used, and present hands-on examples that you can try yourself. It will also help you select the best protocol for your situation, guiding you through questions such as:
- What are your data transfer needs?
- How many sensors will be read and controlled?
- Does your preferred sensor have a default protocol?
Other potential considerations include: error handling, wiring complexity, cost, and even what protocol is dominant in your industry. For example, CAN bus is the standard for communication between systems within a vehicle. The CAN bus protocol is, in fact, so robust and flexible that it is also widely implemented in the equipment that makes automobiles. While one might ask all of these questions for a specific piece of hardware, it's also important to consider your systems holistically, selecting and standardizing sensors in order to facilitate future expansion and maintenance tasks.
In this guide you'll start with the fundamental UART methodology, followed by the versatile I2C protocol, higher-speed SPI, and end with the rugged automotive-industrial CAN protocol. Read on to select and implement the perfect communication method for your sensors!
Chapter 1: UART
UART: Universal Asynchronous Receiver Transmitter – asynchronous signaling method involving discreet RX and TX lines.
UART Characteristics:
- Wiring:
TX
(transmit),RX
(receive) - Range: depends on implementation
- One device to one device
- Speed: typically between 9600 and 115200 bps reference
- Asynchronous: both receiver and transmitter must be set to the same speed prior to transmission.
UART has its roots in the earliest days of computing, even into the telegraph era. While one might refer to UART as a protocol, it might more accurately be called a broad hardware methodology for signal transmission. UART encompasses RS-232, RS-485, and TTL communications, and even current loops formerly used in teletypewriters. Today many applications for UART have been replaced by technologies like USB and Ethernet, but UART is still very much in use for many short-distance applications.
Architecture
UART data transmission can be implemented in software (e.g. bit banging) or via
a built-in computer/microcontroller peripheral that handles communications on a
hardware level. Alternatively, an external UART chip can transmit/receive
parallel data from a processor and transmit/receive data serially via UART RX/TX
lines.
Protocol
A UART TX/RX
pair is typically held at a high voltage when not transmitting
data, and transmission is started by pulling the line low. Data is typically
transferred in the form of packets, which consist of the following.
- Start bit
- Data frame (5 to 9 bits – typically LSB first)
- Parity bit (optional)
- Stop bits (1 to 2 bits line driven from low to high) reference
In the following examples, we will deal with UART in the context of an Arduino Uno and its underlying microcontroller's UART peripheral.
Hands-On UART
For a basic example of UART usage, refer back to our Getting Started With Arduino guide, where we use serial communication to program these boards and get feedback via the serial monitor. The Arduino Uno uses an ATmega328P as its main processor, while a secondary ATmega16U2 chip handles USB-to-UART communications. This acts as a bridge between a modern computer (which typically lacks a serial port) and the Arduino for programming and interface.
UART Loopback: U ARe Transmitting?
You can sense whether or not the UART is actually transmitting and receiving via
a simple loopback test. Connect TX
and RX
pins, allowing the same device to send
and receive signals. Open the Arduino IDE, connect an Arduino Uno via USB, and
select your device.
On an Arduino Uno, connect the TX
and RX
pins (0
and 1
) via a jumper, open the
serial monitor at any baud rate and enter a line. The serial monitor will
receive exactly what you input and print it on screen. The TX
transmits and the
RX
receives via the ATmega16U2. Both are automatically set to the same baud rate
as they're on the same device. Since we're not actually interacting with the
ATmega328P, it's best to connect the reset pin to ground to keep this processor
from sending any signals. You could even remove the ATmega328P altogether and
this loopback test will still work.
Sensor Data Transmission via UART
As an example of direct sensor-to-microcontroller UART usage,
this "Large Ultrasonic (Sonar) Sensor
with Horn and UART Output" from Adafruit fits the bill. Hook up the red sensor
wire to 5V
from the Arduino, and black to GND
. Connect the yellow control wire
to pin 5
, and the white wire to pin 2
.
Load code found here on GitHub, which translates the raw UART data into human-readable text. Open up the Arduino serial port at 115200 baud, and you'll see the received data on display.
Per the device's datasheet, UART data is formatted as four sequential bytes: Frame header, Data_H, Data_L, and checksum. The Hex value Data_H is multiplied by 256 and added to Data_L, which is then converted to decimal by the Arduino to give the value in millimeters. Data is formatted in hex for this particular application, and transmission between the sensor and Arduino takes place at 9600 baud.
UART Bottom Line
If you have a preferred sensor that uses UART for communication, or if you need a very simple device-to-device wiring setup, UART can be an excellent solution. At the same time, this architecture is somewhat limited, and the fact that UART data can come in a variety of formats means you may end up learning how to communicate with it over and over.
Chapter 2: I2C
I2C: Inter-Integrated Circuit (also abbreviated I2C or IIC) – synchronous master/slave protocol developed in 1982 by Philips Semiconductors (now NXP) for communication between processors and peripheral ICs.
I2C Characteristics:
- Wiring: serial data (
SDA
), serial clock (SCL
) - Short distance ~1 meter, possibly longer depending on implementation reference
- Single master, multiple slave architecture
- 7-bit addressing for 128 total addresses, with 16 reserved for special usage reference
- Speed: standard 100 kbit/s
- Synchronous: master generates clock signals reference
I2C was established in 1982 by Philips, and is now freely available for anyone to use and even modify as needed. Several variations exist, including those that use 10-bit addressing, higher speeds at up to 5 Mbit/s, and multi-master communications. What's listed above, however, is typical, and you can expect a bus to behave as such in most situations.
While single master/slave usage is common, I2C's strength lies in interfacing many relatively low-speed slave devices with a single master controller. In the examples, we'll discuss the typical 7-bit address, single-master setup, with a limited number of slaves for simplicity.
Architecture
Wiring for the I2C bus consists of SDA
and SCL
lines. Each device connects to a
common ground, and SDA
and SCL
are each connected to Vcc (typically 5V) via
pull-up resistors (Rp). Rp values are often 4700 ohms, but this value can vary
greatly. Resistors are often built into I2C devices and may not need to be added
separately.
Protocol
On the SCL
line, the master device generates a clock signal, while the SDA
line
is used for both the master and slaves to send information. I2C data is
transmitted in discreet messages, which address one device at a time.
The default line state is high, enforced by the pullup resistors. When sending data, logical 1 is indicated by releasing the line high, while logical 0 is indicated by the line being pulled low. Elaborating and simplifying a bit, the communication process is as follows:
- Start condition set: master pulls
SDA
low, directly followed bySCL
pulled low - Address frame send: master sends a 7-bit address frame, plus 1-bit to indicate read (high/1) or write (low/0). The master device thus requests data from slaves, which do not initiate communication.
- Address acknowledge: slave pulls data line low to indicate it is ready to receive data.
- Data frame: data is sent as 8 bits of data, with logical 1 high, and logical 0 low.
- Data acknowledge: slave pulls data line low to indicate data was received.
- Data frame/data acknowledgement procedure may be repeated as necessary.
- Stop Condition Set: master releases
SCL
high, directly followed bySCL
release high (opposite start condition)
In the case of a read condition, the master device addresses the slave, plus a high read bit. The slave then acknowledges and sends back register data, which the master acknowledges, then sends a stop condition. Prior to this action, the master may write conditions to the slave address with an opcode and register address that corresponds to data the slave will send back.
Hands-On I2C
In the examples below, we'll show how to programmatically find an address, then
take readings from a BM280 temperature/humidity/pressure sensor breakout using a
STEMMA QT connector on the device. An Arduino Uno is used here, but these
examples can be adapted to other hardware. Connect power (red) to the Arduino 5V
port, and black to GND
. Connect yellow to the Arduino SCL
pin, and blue to
SDA
.
What is My Device's I2C address?**
In theory, address information will be found in the product manual and/or PCB
silkscreen. However, this isn't always straightforward. Instead, it's possible
to get your microcontroller to give you a list of connected devices. Simply
connect the I2C bus, with an Arduino Uno as the master device. Run the
code found here
with an I2C device connected, and open the serial monitor at 9600 baud to see
what appears. In this case, there are sensors connected to 0x53
and 0x77
.
The program scans the I2C bus and records what addresses acknowledge communication. This code could easily be adapted to other "non-Arduino" situations as needed.
BME280 as I2C Temperature/Humidity/Pressure Sensor
Removing the second sensor and running the scanner program, we're left with
0x77
for the BME280 I2C
Temperature/Humidity/Pressure sensor breakout. This address can also be found in
the dev board's documentation,
which can be switched to 0x76
via a solder jumper, though you'll need to account
for the non-standard address in software. This particular board also features
hardware that allows it to be used with 3.3V or 5V logic/supplies.
To interface with this device, install the Adafruit BME280 sensor library, along with any needed dependencies (see Chapter 4 of our Arduino guide for more on libraries). Open the demo sketch under File > Examples > Adafruit BME280 Library > bme280test and upload it to your board. Again open up a serial port at 9600 baud, and it will display the temperature, pressure, and humidity. It will also display the approximate altitude, which will typically need adjustment to produce accurate readings.
Here wiring the sensor is very much a plug-and-play affair. As is common for this type of unit, two 10k pullup resistors are built into the breakout board itself, taking care of this detail for it and other devices on the bus. Other sensors can also be connected at the same time, as the correct device will be selected via its specific address.
Breakout: STEMMA QT and Qwiic Connectors
In the world of sensors and connections, STEMMA QT, and the cross-compatible QWIIC standard, make it easy to connect I2C pins together and to a dev board. In the case of the Arduino Uno, one needs to ensure black/red/blue/yellow are plugged into GND/+5V/SDA/SCL via jumpers. Other boards, such as the Blues Notecarrier A pictured below, have QWIIC ports, allowing connectors to be directly attached for plug-and-play I2C operation.
I2C sensor breakouts, such as the BME280 unit featured here, commonly have two ports allowing multiple devices to be daisy-chained as needed. With the I2C bus's wiring architecture, either port can act as an input or an output.
I2C Bottom Line
When you need to interface with a variety of sensors and other devices, with minimal wiring and driver expenses, I2C can be an excellent choice. While its speed and distance capabilities aren't at the top of this list, the I2C protocol is well-supported and more than capable in many situations.
Chapter 3: SPI
SPI: Serial Peripheral Interface – synchronous single-master/multi-slave protocol, introduced by Motorola in the early 1980s. Typically used to communicate between a single master and a limited number of slave devices.
SPI Characteristics:
- Wiring: minimum 4-wire bus (typical)
- Slave select wire needed for each slave device
- Range: Implementation dependent – often 10 cm or less and on single circuit board
- Single-master, multi-slave architecture
- Speed: typically 10 to 20Mbps reference
- Synchronous: Master generates clock signals reference
- De facto standard – many variations
SPI usage was established by Motorola, but is a de facto standard with many implementations. It is therefore important to reference the datasheet for any chip that you'll be using to ensure the details are correct. Origin-wise, SPI is thought to have made its debut in 1983, however, there is some discussion that it was actually introduced in 1979.
Regardless of its true origin details, SPI has a 40+ year history of data transfer and is in widespread use today. Because of its high-speed capability, it's often used as a memory interface and to control displays. A wide variety of sensors and input devices are also available that use this protocol. The limiting factor, when compared to a protocol like I2C, is that a discreet SS line is needed for every slave device. There are techniques to reduce this SS line requirement, though implementation requires
Architecture
Of the four protocols outlined here, SPI has the most complicated wiring architecture, using a total of four lines to transmit data. At the same time, it is a comparatively high-speed protocol in the double-digit Mbps range. It also features full-duplex operation between a single master and only one slave at a time.
Connections consist of the following:
- Synchronous Clock (
SCK
) Reference - Master Out Slave In (
MOSI
) - Master In Slave Out (
MISO
) - Cable Select (
CS
often written with overline to indicate active low) – one required for each slave
The SCK line keeps the master and active slave device coordinated. MOSI
is used
exclusively by the master to transmit to the slave, MISO
is used exclusively by
the slave to transmit to the master, allowing full-duplex operation. CS
is held
high to disconnect the slave from the bus and pulled low by the master to
activate a particular slave, i.e. active low.
If multiple slave devices are to be used, multiple CS
lines normally must be
implemented, one for each device. For the single-master, three-slave
configuration shown below, a total of six control lines would be needed.
Protocol
SPI does not specify frame formats or voltage levels, though typically this
takes place in the form of 8-bit bytes, which can transfer in either a
least-significant bit (LSB) or most-significant bit (MSB) first format. There is
no built-in error checking The CS
pin selects the slave device without a set
address, and SCLK
provides a common clock. The individual implementation
specifies what bits mean.
- Master selects single slave device for communication by pulling
CS
pin low. - Master provides synchronization clock signal through the
SCLK
pin, slave listens forSCLK
andMOSI
signals. - Data is transferred from master to slave via
MOSI
, while any response (or other data) can be simultaneously transmitted from slave to master via MISO. - Bits are registered with respect
SCLK
, typically active high and sampled on the leading edge of the clock pulse, known as a "mode 0" configuration.
There are many variations on the SPI messaging protocol, notably:
- Interrupts may be used in some situations via additional hardware.
- In one-way messaging systems, such as displays, the
MISO
line may be omitted as there is no feedback. - Daisy chain configurations use 1
CS
line for multiple slave devices, passing data from slave to slave via connectedMISO/MOSI
pins. Not every device supports this configuration, and there is a propagation delay as data is sent through the chain.
Hands-On SPI: BME280 as SPI Temperature/Humidity/Pressure Sensor
The BME280 breakout used earlier can transfer data over SPI as well as I2C. The previous I2C BME280 example installs the prerequisites for SPI usage. As before, open File > Examples > Adafruit BME280 Library > bme280test. Comment out line 31 and uncomment 33 to disable I2C and enable software SPI.
Connect the BME280 VIN
and GND
to Arduino 5V
and GND
. Route BME280 pins SCK
,
SDO
, SDI
, and CS
to Arduino pins 13
, 12
, 11
, and 10
respectively. Conveniently,
these latter 4 pins are in-line with respect to an Arduino Uno used here.
Connect the Arduino to your PC via USB, send the modified file to your board,
and open the serial port at 9600 baud to get
temperature/humidity/pressure/altitude readings.
While wiring is not extremely challenging, it is more difficult than other communication options presented in this guide. Where wiring and/or PCB space is at a premium, this can be a real concern, and also means extra potential points of failure. Any additional devices would each need their own slave-select lines, which further add to SPI hardware complications.
SPI Bottom Line
SPI can be a good option when speed and low latency are of utmost concern, and when you're dealing with a limited number of sensors or other devices over a long distance. At the same time, if you need to deal with a multitude of devices in a plug-and-play manner, SPI may be less than ideal if there's another option.
Chapter 4: CAN
CAN: Controller Area Network (CAN bus) – serial transmission protocol with an arbitration scheme that allows nodes on the bus to communicate without a central controller. CAN Bus was released by Robert Bosch GmbH in 1986, and is widely used in automotive and industrial electronics.
CAN Characteristics:
- Wiring: two-wire bus
- Nodes coordinate without a central controller
- Robust differential voltage communication
- Well documented, standardized by ISO
- 40m range at 1Mbps, 1000m range with reduced data speeds
CAN is a well-defined communication protocol, outlined in several ISO standards. This rigidity means that any conforming device should communicate properly within defined parameters, and helps to make wiring and setup straightforward. Originally intended for challenging automotive environments, these same properties also mean that it works well in industrial automation, where rock-solid communication within and between manufacturing equipment is paramount.
With relatively high speeds, excellent reliability, and good transmission range, CAN has become the protocol of choice for automotive applications, and is very common in industry. At the same time, CAN works at differential voltage levels that microcontrollers and similar computing hardware can't generate on their own, requiring additional hardware for usage. This additional expense and circuit complication often means this protocol is pushed aside in favor of other options.
Architecture
On a bus level, CAN wiring is quite simple. This consists of CAN High (CAN-H
)
and CAN Low (CAN-L
) lines, onto which each node connects via shorter stubs. A
120 ohm resistor should be placed at each end of the bus across CAN-H
and CAN-L
to reduce reflections and keep the system in a predictable state.
Additionally, each node has its own communication setup, as the microcontroller
or other device will not connect to the bus directly. Instead, a CAN controller
is used to translate microcontroller signals into the CAN bus protocol. A CAN
transceiver then interacts with the bus directly, generating specific CAN-H
and
CAN-L
voltages.
Some microcontrollers (e.g. Espressif's ESP32) have CAN controller functionality built-in, requiring only transceiver hardware to communicate via a CAN bus. Devices that do not have this functionality may still communicate with CAN, but will need to pass signals to an external CAN controller (often via SPI). In such implementations, the CAN controller and transceiver may be combined into one chip (e.g. Microchip's MCP25625).
Protocol
CAN bus data frames consist of an ID/arbitration number, followed by control signals, up to 8 bytes of data, checks, and end-of-frame bits. One might say the "killer app" for CAN is that multiple nodes can exist on one bus, transmitting and receiving messages without a system-critical master device.
CAN bus signaling works via the differential voltage between the CAN-H
and CAN-L
lines, each of which idles at 2.5V. CAN-H
is driven to 3.5V and CAN-L
is driven
to 1.5V to form a 2V dominant differential, signifying a 0
condition. During
idle at ~0V differential, or when CAH-H
and CAN-L
are driven so that the CAN-H
voltage is less than or equal to CAN-L
in a recessive differential state, a
1 bit is indicated.
Because of this electrical setup, if one node sends a recessive bit (~0V differential, i.e. 1) and another node sends a dominant bit (2V differential, 0) at the same time, the overall bus is pulled to dominant (0). Node messages are prioritized based on which message has the lowest identifier MSB first. So if messages A and B were sent with the 11-bit identifiers:
- Message/Node A: 00000000011
- Message/Node B: 00000000001
Node B would win the arbitration. Node A has sent a 1 bit in position 10, but receives a 0 instead since Node B is transmitting this state and knows it has lost arbitration. Node B continues with its transmission with no loss of data. Node A will try again later. CAN bus nodes must therefore be able to not only transmit differential signals, but simultaneously sense and react to the results. Including arbitration, a simplified explanation of the CAN data frame format is as follows:
- Start of frame
- Arbitration
- Control bits
- Data bits (0-64 bits, divided up into 0-8 bytes)
- Cyclic redundancy check (CRC - 15 bits)
- End-of-frame (EOF)
- Inter-frame spacing (IFS)
Adding up bits in a CAN bus frame, for a 64-bit message, a full 112 bits are transmitted in order to route and properly verify a message. This is a significant amount of overhead, but between its brilliant hardware design and built-in software checks, CAN signaling is extremely reliable. Contrast this to a protocol like SPI–where bits are sent with no extra hardware or error checking/addressing overhead–and one must carefully consider what is best for a particular implementation.
Breakout: Automobile OBD2 Diagnostic Port and CAN Bus
Every day, countless automobiles transmit data internally between their ECUs (Engine Control Units) and a variety of sensors using CAN. These individual interactions go largely unnoticed by their human operators, instead manifesting themselves as responses to inputs like steering and acceleration, and informational outputs like gauges and readouts.
It is, however, possible to tap into a vehicle's communication system via the OBD2 port, a standard connector that has been mandatory for vehicles sold in the US since 1996. While the port has been standard for over a quarter of a century, the CAN protocol was standardized over a decade later, in 2008. The upshot is that professional or amateur mechanics can use the OBD2 port (most likely operating via the CAN protocol) to diagnose problems and even facilitate enhancements.
Hands-On CAN Bus
For our CAN example, we will turn to this guide's host, Blues, and their CAN bus Vehicle Monitor accelerator reference application. Their how-to guide shows how to set up a USB-to-CAN adapter to send CAN bus messages to an Adafruit Feather M4 CAN Express via a development PC. The CAN Express Feather filters CAN messages, and if they match the proper criteria, passes them along to a Blues Notecard via a Notecarrier F interface board.
This received data can then be uploaded to the cloud. This same methodology could be used with a vehicle OBD-II port adapter on an automobile, allowing for remote vehicular monitoring. These techniques could also be appropriate for CAN-enabled industrial equipment (or other devices), allowing for remote equipment and sensor monitoring from anywhere with a cellular connection.
Thanks for Reading!
We hope this guide has been useful for you to get started with four interface methodologies that drive much of the world of IoT: UART, I2C, SPI, and CAN. Each has its own strengths and weaknesses, and there are many instances where several protocols might nominally work, but one is best. Careful planning is needed to help alleviate problems later.
As the quote goes:
The great enemy of communication is the illusion it's taken place.
This is at least as true with machines as it is with humans. With the correct protocols in place we can ensure that data flows to its proper destination with sufficient speed and fidelity. If the eventual destination happens to be "the cloud," Blues has a range of answers to help with your transmission needs!
Blues University - Next Steps
This article if part of a broader series where you can dig deeper into each aspect of embedded development. To embark on this journey, be sure to follow Blues University, where you can explore and contribute to shaping the future of IoT.
If you're new to embedded development and the IoT, the best place to get started is with the Blues Starter Kit for Cell + WiFi and then join our community on Discourse. 💙
Jeremy Cook is an engineer and tech journalist who has spent over a decade in process automation. You can find him on Twitter, YouTube, and elsewhere on the Internet, where he’s best known for his electronics experimentation and robotics builds.