ENS160 Digital Metal-Oxide Multi-Gas Sensor

The ENS160 is a digital MOX gas sensor optimized for indoor air quality monitoring. It provides accurate measurements of TVOC and eCO₂ levels, along with AQIs, making it ideal for applications in building automation, HVAC systems, and air purifiers. The sensor supports both I²C and SPI interfaces, allowing for flexible integration into various microcontroller platforms.

ENS160 Digital Metal-Oxide Multi-Gas Sensor image
ENS160 · I2C
I2C
Interface
8pins
Connections
-40 to 85 °C
Operating temp
$10 per piece
Typical price
On this page

ENS160 pinout

8 pins · I2C

The ENS160 has 8 pins supporting I²C or SPI communication with additional control signals.

View:
ENS160 Digital Metal-Oxide Multi-Gas Sensor pinout
PinTypeDescriptionNotes
VINPowerPower supply input (1.71V-1.98V for VDD; 1.71V-3.6V for VDDIO).Use 3.3V for most breakout boards with onboard regulation.
GNDPowerGround connection. Connect to system ground.
SDAI2CI²C data line. Bidirectional data communication.Connect to ESP32 GPIO 21. Requires pull-up resistor.
SCLI2CI²C clock line. Clock signal for I²C communication.Connect to ESP32 GPIO 22. Requires pull-up resistor.
INTInterruptInterrupt output (optional). Indicates data ready or threshold events.Active low. Connect to GPIO for interrupt-driven operation.
WAKEControlWake-up control input. Used for power management.Pull high for normal operation.
ADDRAddressI²C address selection. Connect to GND (0x52) or VDD (0x53).Default varies by module.
CSControlChip select for SPI mode. Tie to VIN (3.3V) for I²C mode - low selects SPI.Must be high (VIN) for I²C operation.
  • I²C address: 0x52 (ADDR to GND) or 0x53 (ADDR to VDD)

  • Measures TVOC, eCO₂, and AQI (Air Quality Index)

  • Four independent MOX sensor elements with hotplate control

  • Superior gas selectivity vs CCS811

  • Detects: ethanol, toluene, H₂, NO₂, and more

  • On-chip humidity/temperature compensation

Wiring the ENS160 to ESP32

8 connections · 1 optional

To interface the ENS160 with an ESP32 via I²C, connect VIN to 3.3V, GND to ground, SDA to GPIO 21, SCL to GPIO 22, CS to VIN (for I²C mode - low selects SPI), and WAKE to VIN.

ENS160 Digital Metal-Oxide Multi-Gas Sensor wiring with ESP32
ENS160 pinESP32 pinPurpose
VIN3.3VPower supply. Use 3.3V (breakout boards have regulation).
GNDGNDGround connection.
SDAGPIO 21I²C data line. Add 4.7kΩ pull-up resistor.
SCLGPIO 22I²C clock line. Add 4.7kΩ pull-up resistor.
CSVIN (3.3V)Chip select. Must be high (VIN) for I²C mode - low selects SPI.
WAKEVIN (3.3V)Wake control. Pull high for continuous operation.
ADDRGND or VINAddress select. GND=0x52, VIN=0x53.
INTOptional GPIOInterrupt output for data ready indication. · optional
  • I²C pull-up resistors (4.7kΩ) required on SDA and SCL

  • Default I²C address: 0x52 or 0x53 (check your module)

  • CS pin must be tied high (VIN/3.3V) for I²C operation - pulling it low selects SPI (many breakout boards pull it high already)

  • WAKE pin must be pulled high for normal operation

  • Use ScioSense_ENS160 or DFRobot_ENS160 library

  • Warm-up time: 1 hour for optimal accuracy

  • Combine with AHT21/SHT sensor for humidity/temp compensation

  • Add decoupling capacitor (100nF) close to VIN pin for stability

ENS160 code examples

5 platforms
Platform:

ENS160 Arduino example

Copy
// Requires library: "ENS160 - Adafruit Fork"
#include <Wire.h>
#include "ScioSense_ENS160.h"

ScioSense_ENS160 ens160(ENS160_I2CADDR_1); // 0x53 (ADDR high); use ENS160_I2CADDR_0 for 0x52

void setup() {
    Serial.begin(115200);
    Wire.begin();
    if (!ens160.begin()) {
        Serial.println("Failed to initialize ENS160 sensor!");
        while (1);
    }
    ens160.setMode(ENS160_OPMODE_STD); // standard gas-sensing mode
}

void loop() {
    if (ens160.available()) {
        ens160.measure(true);
        Serial.print("eCO2: ");
        Serial.print(ens160.geteCO2());
        Serial.print(" ppm, TVOC: ");
        Serial.print(ens160.getTVOC());
        Serial.print(" ppb, AQI: ");
        Serial.println(ens160.getAQI());
    }
    delay(1000);
}

The example uses the maintained "ENS160 - Adafruit Fork" library (its header keeps the original ScioSense_ENS160.h name). The sensor is created at address 0x53 (ENS160_I2CADDR_1, ADDR pin high - use ENS160_I2CADDR_0 for 0x52), put into standard gas-sensing mode with setMode(ENS160_OPMODE_STD), and each cycle measure() refreshes the values read by geteCO2(), getTVOC() and getAQI() (UBA index 1-5). Give the sensor a warm-up period after power-on before trusting the readings.

ENS160 ESP-IDF example

Copy
// Requires the esp_ens160 driver from the ESP Component Registry:
//   idf.py add-dependency "k0i05/esp_ens160^1.2.7"

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2c_master.h"
#include "ens160.h"

#define SDA_GPIO GPIO_NUM_21
#define SCL_GPIO GPIO_NUM_22

void app_main(void)
{
    i2c_master_bus_config_t bus_config = {
        .clk_source = I2C_CLK_SRC_DEFAULT,
        .i2c_port = I2C_NUM_0,
        .sda_io_num = SDA_GPIO,
        .scl_io_num = SCL_GPIO,
        .flags.enable_internal_pullup = true,
    };
    i2c_master_bus_handle_t bus = NULL;
    ESP_ERROR_CHECK(i2c_new_master_bus(&bus_config, &bus));

    ens160_config_t config = I2C_ENS160_CONFIG_DEFAULT;
    ens160_handle_t sensor = NULL;
    ESP_ERROR_CHECK(ens160_init(bus, &config, &sensor));

    while (1) {
        ens160_air_quality_data_t data;
        if (ens160_get_measurement(sensor, &data) == ESP_OK)
            printf("AQI-UBA %d, TVOC %u ppb, eCO2 %u ppm\n",
                   data.uba_aqi, data.tvoc, data.eco2);
        else
            printf("Could not read data from sensor\n");
        vTaskDelay(pdMS_TO_TICKS(2000));
    }
}

ESP-IDF ships no ENS160 driver of its own, so this example uses the k0i05/esp_ens160 driver from the ESP Component Registry. Install it into your project first with idf.py add-dependency "k0i05/esp_ens160^1.2.7", then build as usual.

The driver uses ESP-IDF's modern i2c_master API, so the code first creates an I2C master bus and then hands it to ens160_init(). ens160_get_measurement() returns the UBA air-quality index (1 = excellent to 5 = unhealthy), TVOC in ppb and eCO2 in ppm. The default configuration expects the ENS160 at address 0x53; if your breakout pulls ADDR low, set config.i2c_address = I2C_ENS160_DEV_ADDR_LO (0x52) before calling ens160_init(). Give the sensor a few minutes of warm-up before trusting the readings.

ENS160 ESPHome example

Copy
i2c:
  sda: GPIO21
  scl: GPIO22

sensor:
  - platform: ens160_i2c
    eco2:
      name: "eCO2"
    tvoc:
      name: "TVOC"
    aqi:
      name: "Air Quality Index"
    address: 0x53
    update_interval: 1s

Since ESPHome 2024.6 the platform is split by bus - ens160_i2c here, with the sensor at address 0x53 (ADDR pin high; use 0x52 with ADDR low) on the default I2C pins. eCO2, TVOC and the AQI each become entities. For best accuracy pair it with a temperature/humidity sensor and add compensation: options.

ENS160 PlatformIO example

Copy
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
lib_deps =
    adafruit/ENS160 - Adafruit Fork @ ^3.0.1
src/main.cppCopy
#include <Arduino.h>
#include <Wire.h>
#include "ScioSense_ENS160.h"

ScioSense_ENS160 ens160(ENS160_I2CADDR_1); // 0x53 (ADDR high); use ENS160_I2CADDR_0 for 0x52

void setup() {
    Serial.begin(115200);
    Wire.begin();
    if (!ens160.begin()) {
        Serial.println("Failed to initialize ENS160 sensor!");
        while (1);
    }
    ens160.setMode(ENS160_OPMODE_STD); // standard gas-sensing mode
}

void loop() {
    if (ens160.available()) {
        ens160.measure(true);
        Serial.print("eCO2: ");
        Serial.print(ens160.geteCO2());
        Serial.print(" ppm, TVOC: ");
        Serial.print(ens160.getTVOC());
        Serial.print(" ppb, AQI: ");
        Serial.println(ens160.getAQI());
    }
    delay(1000);
}

This PlatformIO example interfaces with the ENS160 sensor using SDA (GPIO21) and SCL (GPIO22). The sensor is initialized with the I²C address set to 0x53. eCO₂, TVOC, and AQI values are retrieved in the loop() and printed to the Serial Monitor every second.

ENS160 MicroPython example

Copy
from machine import I2C, Pin
import time

# ENS160 I2C address
ENS160_ADDR = 0x53

# Register addresses
DATA_ECO2 = 0x22
DATA_TVOC = 0x24
DATA_AQI = 0x26

# Initialize I2C
i2c = I2C(0, scl=Pin(22), sda=Pin(21))

# Function to read 16-bit register
def read_register(addr, reg):
    data = i2c.readfrom_mem(addr, reg, 2)
    return (data[0] << 8) | data[1]

while True:
    eco2 = read_register(ENS160_ADDR, DATA_ECO2)
    tvoc = read_register(ENS160_ADDR, DATA_TVOC)
    aqi = i2c.readfrom_mem(ENS160_ADDR, DATA_AQI, 1)[0]

    print(f"eCO2: {eco2} ppm, TVOC: {tvoc} ppb, AQI: {aqi}")
    time.sleep(1)

This MicroPython script interfaces with the ENS160 sensor over I²C using SDA (GPIO21) and SCL (GPIO22). It reads eCO₂, TVOC, and AQI data by accessing the corresponding registers. The read_register() function retrieves 16-bit data from the sensor. The main loop continuously fetches and prints the sensor values every second.

ENS160 specifications

From the datasheet
Measurement Parameters
TVOC, eCO₂, AQI
Operating Voltage
1.71V to 1.98V (VDD); 1.71V to 3.6V (VDDIO)
Interface
I²C, SPI
Operating Temperature
-40°C to 85°C
Operating Humidity
5% to 95% RH (non-condensing)
Package
3 x 3 x 0.9 mm LGA

About the ENS160

The ENS160 is ScioSense’s follow-up to the CCS811, and it is explicitly marketed - by ScioSense and by module vendors alike - as that older part’s replacement now that the CCS811 is obsolete. Where the CCS811 has a single metal-oxide element, the ENS160 packs four independent MOX elements with their own hotplate control, which the datasheet credits for better gas selectivity and steadier output across humidity and ozone. Like the CCS811, though, its eCO2 output is a VOC-derived estimate rather than a direct CO2 measurement - for the real thing, an NDIR sensor like the MH-Z19 is the right part, the same tradeoff that applies to Bosch’s BME680 when it is run through BSEC.

What makes the ENS160 genuinely easier to use is that the eCO2, TVOC and AQI numbers are computed on-chip: ScioSense’s own datasheet describes on-chip heater drive control and data processing with no external libraries needed, unlike Bosch’s BME680/BME688, which need the closed-source BSEC library just to turn a raw resistance into a comparable number. The chip exposes both I2C and SPI, so it drops into either bus depending on what a project already has free.

There are two warm-up periods worth planning around. A one-hour “Initial Start-up” runs exactly once in the sensor’s life, the first time it is ever powered on, since the raw resistance signals shift the most in that first stretch; a shorter 3-minute “Warm-up” then applies after every later power-cycle or idle period. The catch is that the one-hour milestone is only saved to non-volatile memory after 24 hours of continuous operation - power the sensor off before that, and the next boot restarts the first-ever hour from scratch.

ENS160 troubleshooting

4 common issues

Compilation Error: 'TwoWire::begin(uint8_t&, uint8_t&)'

›

Issue: When compiling code for the ENS160 sensor, the following error occurs: no matching function for call to 'TwoWire::begin(uint8_t&, uint8_t&)'.

Possible causes include the sensor library attempting to call a non-standard Wire.begin() function with two arguments, which is not supported by all platforms.

Solution: Modify the library code to use the standard Wire.begin() function without arguments. Specifically, in the ScioSense_ENS160.cpp file, locate the _i2c_init() function and replace its contents with a single call to Wire.begin();. This change ensures compatibility across different platforms.

ENS160 Readings Unavailable in ESPHome

›

Issue: The ENS160 sensor intermittently reports unavailable readings in ESPHome, with log messages indicating: ENS160 readings unavailable - Normal Operation but readings not ready.

Possible causes include insufficient power supply stability, especially when the sensor is combined with other components like the AHT21.

Solution: Enhance the power supply stability by adding a supplemental capacitor to the sensor's power lines. This addition helps prevent voltage dropouts and ensures consistent sensor operation.

ENS160 Fails to Initialize with WiFi Enabled on ESP32

›

Issue: When using the ENS160 sensor with an ESP32, enabling WiFi causes the sensor to report zero or no data on all channels.

Possible causes include resource conflicts or timing issues between the WiFi module and the I2C communication used by the sensor.

Solution: Investigate potential conflicts between the WiFi and I2C peripherals on the ESP32. Ensure that the I2C bus is properly configured and consider adjusting the I2C clock speed. Additionally, test the setup with WiFi disabled to confirm that the issue is related to WiFi interference.

ENS160 Not Detected on I2C Bus

›

Issue: The ENS160 sensor is not recognized on the I2C bus, leading to initialization failures.

Possible causes include incorrect I2C address configuration or improper wiring.

Solution: Verify the sensor's I2C address, which can be either 0x52 or 0x53, depending on the state of the address selection pin (ASW). Ensure that the ASW pin is set correctly to match the expected address in your code. Additionally, check all wiring connections for correctness and secure contact.

Where to buy the ENS160

ENS160 Digital Metal-Oxide Multi-Gas Sensor
ENS160 Digital Metal-Oxide Multi-Gas Sensor
$10 per pieceper unit, typical
Amazon and AliExpress links are affiliate links - buying through them supports the site at no extra cost to you.

Resources