Download PDF Parts List View on YouTube Download Code

Today, we will take a deep dive into using I²C with the ESP32 microcontroller. We’ll examine the I²C implementation in the ESP32 and see how we can use it as a Controller, Peripheral, and with multiple I²C buses.

Introduction

The I²C (Inter-Integrated Circuit) bus is one of the most versatile and popular communication protocols used by microcontrollers. Whether you’re connecting sensors, displays, or other peripherals to your ESP32, understanding how to use I2C effectively can significantly expand your project possibilities.

In this article and accompanying video, we’ll explore the ESP32’s I²C capabilities from the ground up. We’ll start with the fundamentals of the I²C protocol, then dive into the ESP32’s specific implementation. You’ll learn how to connect and communicate with I²C devices, utilize multiple I²C buses simultaneously, and even configure your ESP32 to act as an I²C peripheral device.  That last task will allow you to start designing your own custom I²C Peripherals.

Whether you’re a student, hobbyist, or an experienced maker, you’ll find something useful in this practical guide to I²C on the ESP32. Let’s get started!

The I²C Bus

The Inter-Integrated Circuit (I²C or I2C) bus is a simple, low-speed, short-distance communication protocol developed by Philips (now NXP) in 1982. It is widely used for connecting sensors, displays, EEPROMs, ADCs/DACs, real-time clocks, and other peripherals to microcontrollers such as Arduino and ESP32. Unlike UART or SPI, I²C uses only two wires for communication:

  • SDA (Serial Data line) – carries the data.
  • SCL (Serial Clock line) – carries the clock signal.

The bus also included wires for VCC (3.3 or 5 volts) and Ground.

I²C devices can be divided into two categories:

  • Controller – This is the host of the I²C bus. It provides the clock signal on the SCL line and initiates all communications. The Controller was formerly called the “Master”.
  • Peripheral – These are the “clients” of the bius. They respond to communications from the Controller. Peripherals can be input or output devices, and examples include sensors and displays. The Peripheral was formerly referred to as a “Slave” device.

Each Peripheral on the I²C bus has a unique 7-bit address (there are also 10-bit address I²C devices). The Controller uses this address to talk to specific peripherals. It is possible to have multiple controllers, but only one controller can be active at a time. Multiple controllers are not common, and we won’t be implementing this type of configuration today.

The Controller includes the intended peripheral’s address in the first byte of every I²C transaction. No two peripherals on the same bus should share an address, or bus conflicts will occur.  In theory, up to 127 peripherals can exist on the same bus; however, in real life, the number of devices usually doesn’t exceed a dozen.

Data is sent in 8-bit packets, and every byte transferred is followed by an acknowledgment bit from the receiving device.  Only the Controller can initiate a conversation, preventing conflicts between peripherals.  The Controller is also the source of the clock signal on the SCL line, and all bus traffic is synchronized to this clock.

The bus uses open-drain outputs with pull-up resistors on both SDA and SCL lines, typically 4.7k to 10k. Many I2C peripherals have built-in pull-up resistors, and care should be taken to keep the total resistance of all pull-ups (wired in parallel) to over 2.2k. Most designs aim for a 4.7k pull-up resistor. 

The I²C bus can operate at different logic voltage levels, and is commonly implemented with either a 3.3-volt or 5-volt power supply. If you need to mix peripherals and controllers that use different logic voltages, you’ll need to use a Voltage Converter module. It must be bidirectional, as data on the SDA line flows in both directions.

ESP32 I²C Basics

The ESP32 family includes robust I²C support with hardware controllers that handle the low-level protocol details. Most ESP32 variants feature two built-in hardware I²C controllers (sometimes called “I²C ports”):

  • I²C Controller 0 (I2C0 (I2C_EXT0))
  • I²C Controller 1 (I2C1 (I2C_EXT1))

These controllers are part of the chip’s peripheral system and can operate simultaneously, allowing for flexible multi-bus configurations.

ESP32 I²C Implementation

The hardware controllers can be configured independently to operate in either Controller mode (issuing the clock and initiating transactions) or Peripheral mode (responding to requests from another I²C controller on the bus). This dual capability allows you to use an ESP32 both as a system “host” talking to sensors, and as a smart peripheral that another microcontroller can control.  We will be configuring an ESP32 as a Peripheral later in this article.  Note that you will need to program using the ESP-IDF to use the second controller in Peripheral mode.

Unlike many microcontrollers with fixed I²C pins, the ESP32 lets you map SDA and SCL to almost any available GPIO. In some iterations (such as the ESP32-C6 that we will be using today), the second I²C bus is mapped to fixed pins.

ESP32-C6 DevKit

The ESP32-C6-DevKitC-1 is Espressif’s official development board for the ESP32-C6, a powerful Wi-Fi 6 and Bluetooth 5 (LE) enabled RISC-V microcontroller. 

This board is designed for prototyping and testing applications that take advantage of the ESP32-C6’s next-generation connectivity features. It includes a USB-to-UART bridge for programming, a 5V-to-3.3V regulator, a reset button, and a boot button, making it ready for use with the Arduino IDE, ESP-IDF, or other frameworks.

The DevKit has the following specifications:

  • Module: ESP32-C6-WROOM-1 or WROOM-1U (external antenna)
  • Flash Memory: 8 MB SPI flash
  • Wireless Protocols:
    • Wi-Fi 6 (802.11ax, 2.4 GHz)
    • Bluetooth 5 (LE)
    • IEEE 802.15.4 (Zigbee 3.0, Thread 1.3)
  • Processor: Single-core RISC-V CPU
  • USB Ports:
    • USB-to-UART bridge (up to 3 Mbps)
    • Native USB 2.0 (12 Mbps, full-speed)
  • Power Supply Options:
    • USB Type-C ports
    • 5V and 3.3V pin headers
  • GPIO Access: Most GPIOs broken out to pin headers
  • RGB LED: Addressable, connected to GPIO8
  • Buttons:
    • Boot (for flashing)
    • Reset
  • Current Measurement: J5 jumper for inline ammeter connection

With support for protocols like Matter and Zigbee, as well as the standard WiFi and Bluetooth, this inexpensive module is one that I’ll be using for many projects going forward.

ESP32 I²C Demo Hookup

We will be doing a few experiments using the ESP32-C6 DevKit and the following I²C peripherals:

  • Adafruit AHT20 Temperature and Humidity Sensor
  • SSD1306 OLED Display (we will be using two of these eventually)

Here is how we will be hooking everything up:

If you don’t have the same ESP32 module as the one I am using, you can substitute another one, as just about any ESP32 will work. If GPIO pins 4 and 5 are not available, you can use different ones and change the pin designations in the sketches to match.

ESP32 I²C Demo – Basic Wire Code for AHT20

We will begin by creating a sketch to display temperature and humidity readings from the AHT20 on the serial monitor.  We will be ignoring the OLED for now.

Instead of using a library for the AHT20,  we are just going to use the Wire library. This will show you how to communicate with an I²C peripheral without a dedicated library. The code implements the complete AHT20 communication protocol from scratch, providing excellent insight into low-level I²C operations.


The program follows the AHT20’s required initialization sequence: performing a soft reset, sending calibration commands, and then entering a continuous measurement loop. Each measurement cycle involves triggering a reading, waiting for the sensor to complete its internal conversion process, and then retrieving six bytes of raw data. 

Here is the sketch:

In the setup() function, the sketch initializes serial communication for output and starts the I²C bus using custom SDA and SCL pins (GPIO 4 and 5). It then sends a soft reset command (0xBA) to the AHT20, followed by an initialization/calibration command (0xBE with parameters 0x08, 0x00). These steps ensure the sensor starts in a clean state and is ready for measurements.

Inside the loop() function, the ESP32 sends the trigger measurement command (0xAC, 0x33, 0x00) to the sensor, which starts a combined temperature and humidity measurement. After waiting about 80 ms for the measurement to complete, the ESP32 requests 6 data bytes from the sensor. The sketch checks the busy flag in the first byte to confirm the sensor is ready, then extracts the raw 20-bit humidity and temperature values by combining bits from multiple bytes. 

These raw values are converted into percentage humidity and degrees Celsius using formulas from the AHT20 datasheet. Finally, the results are printed to the serial monitor every two seconds.

Load the sketch and observe the output on the serial monitor. If you don’t get any output, double-check your wiring.

ESP32 I²C Demo – AHT20 with OLED Display

Now we will add the OLED display to the picture. We’ll simplify our coding by using three libraries:

  • Adafruit_AHTX0 – Library for the AHT20 temperature and humidity sensor.
  • Adafruit_GFX – Graphics library, used with the display.
  • Adafruit_SSD1306 – Library for the SSD1306 OLED display.

All of these libraries can be installed using the Library Manager in the Arduino IDE.

Here is the sketch we will be using:

In setup(), the code starts the serial monitor, then brings up the I²C bus on custom pins SDA=GPIO4 and SCL=GPIO5 with Wire.begin(SDA_PIN, SCL_PIN). It then initializes each Peripheral in turn. If the OLED or the sensor isn’t detected at its address, the sketch prints a message and stops.

In the loop() function, the ESP32, acting as the Controller, requests the latest temperature and humidity data from the AHT20 Peripheral using the aht.getEvent() function. Once the data is received, the code sends it to the Serial Monitor for debugging and then formats it to be displayed on the OLED screen. It clears the previous readings from the display and sends the new values, which the OLED Peripheral then shows on its screen. 

This cycle repeats every two seconds.

Load the code and observe the OLED display. You should see the temperature and humidity displayed, along with a header. On most OLEDs, the header will be a different color than the main text area; my header is yellow with blue text.

Multiple I²C Buses 

The ESP32 has two hardware I²C controllers, each one capable of operating as either a Controller or a Peripheral.  Note that limitations in ESP32 Boards Manager 3 prevent the second bus from being used as a Peripheral; you would need the ESP-IDF development platform to do this.

One great use of the second I²C bus is to resolve a problem when two peripherals have the same address, and neither one can be changed. We will simulate this situation with two OLED displays, each with the address of 0x3C. While this situation would normally cause a conflict, we can put one display on the second I²C bus to avoid this.

ESP32 I²C Multiple Bus Demo Hookup

Here is our hookup. Note that this is the same hookup we’ve already made; we are just adding a second OLED.

Note that GPIO6 and GPIO7 are the default pins for the second I²C controller. If you are using a different ESP32, you will need to determine the second controller’s default pins, as the Arduino IDE won’t allow you to change them.

ESP32 I²C Multiple Bus Demo Code

Here is the sketch that we will be using to utilize both I²C buses, each one driving an identical OLED display.

The sketch configures the ESP32 to act as a Controller on two separate I²C buses simultaneously. Instead of using the default Wire object, the code creates two distinct TwoWire objects: I2C_Bus0 and I2C_Bus1. Each object is assigned to one of the ESP32’s physical hardware controllers (0 and 1) and is connected to its own unique set of SDA/SCL pins.

The code then initializes three Peripheral devices:

  • An AHT20 sensor and the first OLED display (tempDisplay) are initialized on Bus 0.
  • The second OLED display (humidityDisplay) is initialized on Bus 1.

In the loop(), the Controller first requests data from the AHT20 sensor on Bus 0. It then sends the temperature value to the OLED on Bus 0 and the humidity value to the OLED on Bus 1, updating each display with its specific information. 

The setup() function also includes a handy I²C scanner that checks both buses to verify which peripherals are connected to each one.

Load the code to the ESP32 and observe the results. You should see temperature displayed on one OLED and humidity on the other. The I²C address of each OLED (ox3C) is also shown.

ESP32 as an I²C Peripheral

This demonstration code transforms the ESP32-S3 into a custom I²C peripheral device, creating what is essentially a “smart potentiometer sensor” that can be queried by any I²C controller. The code showcases the ESP32’s ability to operate as a peripheral rather than the typical controller role we’ve seen in previous examples.

The implementation uses a Seeeduino XIAO ESP32-S3 with a potentiometer connected to analog pin A0. The ESP32 continuously reads the potentiometer value, applies sophisticated averaging to reduce noise, and maps the 12-bit ADC reading to an 8-bit value (0-255) for efficient I²C transmission. When an I²C controller requests data from address 0x2A, the ESP32 responds with the current potentiometer value as a single byte.

We are using the XIAO as a peripheral for two reasons:

  • It is small, which is good for a DIY peripheral.
  • The ESP32-C6 cannot be programmed as a peripheral using the Arduino IDE (it can with the ESP-IDF).

One restriction of the Wire Library is that a Peripheral must use the default I²C pins.

ESP32 I²C Peripheral Demo Hookup

Here is the hookup for our demo. Note that we are still using an ESP32-C^ DevKit as the Controller. Actually, any ESP32 would probably work as a controller.

Also note the use of the pull-up resistors, which were not required in previous circuits, as the peripherals (AHT20 & OLED) both have built-in ones.

ESP32 I²C Peripheral Demo Code – Peripheral

Here is the sketch that we will load onto the Seeeduino XIAO:

The setup() function is where the ESP32 is configured to act as a Peripheral. The Wire.begin(I2C_ADDR) command initializes the I²C bus and assigns the device the specific address of 0x2A. The two most important lines are Wire.onRequest(onI2CRequest) and Wire.onReceive(onI2CReceive). These register “callback” functions, telling the Wire library which functions to automatically run whenever the controller interacts with it.

The loop() function is very simple. It continuously calls the readPotByte() function to get the current position of the potentiometer and stores the result in the potByte variable. The readPotByte function is designed for accuracy, taking eight quick readings from the analog pin and averaging them to reduce electrical noise before mapping the result to a single byte.

The core of the peripheral functionality is the onI2CRequest() function. This function is automatically executed by the Wire library only when an I²C Controller sends a request for data to address 0x2A. When that happens, this function takes the last value read from the potentiometer (stored in potByte) and sends it out onto the I²C bus for the controller to read.

ESP32 I²C Peripheral Demo Code – Controller

The Controller is an ESP32-C6 DevKit module. Here is the code that we will be loading onto it:

The setup() function initializes the ESP32 for its role as the bus Controller. It starts the Wire library, explicitly assigning GPIO4 and GPIO5 as the SDA and SCL pins. It also sets the I²C communication speed to a standard 100 kHz.

The loop() function contains the repeating action of the controller. It uses the Wire.requestFrom() command to ask the peripheral device at address 0x2A for exactly one byte of data. The code then checks to ensure a byte was successfully received. If it was, the controller reads the byte using Wire.read() and prints the value to the serial monitor. If the request fails for any reason (like incorrect wiring or the wrong peripheral address), it prints an error message. This entire process repeats approximately five times per second.

Load up both sketches and observe the serial monitor on the Controller. You should see the value of the potentiometer, from 0 to 255, depending on its position.

While this is a simple sketch, the principle that it demonstrates can be used to construct your own advanced I²C peripherals using an ESP32.

Conclusion

I²C is an incredibly useful bus arrangement, allowing you to simplify the connection of sensors, displays, and other peripherals. Now that you are familiar with the advanced I²C functions within the ESP32, you can use I²C devices more efficiently in your projects.

It’s time to get on the bus! Hope you enjoyed the article and video.

Parts List

Here are some components that you might need to complete the experiments in this article. Please note that some of these links may be affiliate links, and the DroneBot Workshop may receive a commission on your purchases. This does not increase the cost to you and is a method of supporting this ad-free website.

Espressif ESP32-C6 DevKit        Mouser      DigiKey

Seeduino XIAO ESP32-S3           Mouser      DigiKey

Adafruit AHT20                            Mouser      DigiKey

SSD1306 OLED                            Amazon

 

Resources

Code for this article – All the code in an easy-to-use ZIP file.

PDF Article – PDF version of the article, also in a ZIP file.

Espressif ESP32-C6 DevKit – User Guide for Espressif ESP32-C6 Dev Kit

Seeeduino XIAO ESP32-S3 – Seeed Studio Wiki for the XIAO ESP32-S3

 

I²C Tricks and Tips with ESP32
Summary
I²C Tricks and Tips with ESP32
Article Name
I²C Tricks and Tips with ESP32
Description
Learn how the I²C bus operates and study the implementation of I²C in the ESP32 microcontroller. In this article we'll learn how to us the I²C bus, how to work with multiple busses and how to build an I²C peripheral.
Author
Publisher Name
DroneBot Workshop
Publisher Logo
Tagged on:

0 Comments
Oldest
Newest
Inline Feedbacks
View all comments