ENS160 Air Quality Sensor

ENS160 Air Quality Sensor 
Modules

This section deals with the configuration of the ENS160 air quality sensor. Two versions of ENS160 sensor module have been tested, a basic version that simply uses the ENS160 air quality sensor and another that also includes an AHT21 temperature and humidity sensor. Both modules are provided with I2C and SPI interfaces and can be configured to trigger an interrupt when pollutant levels exceed a preset value.

*** NOTE ***

While I have presented configuration details for both modules below, I have not been able to get the [purple] ENS160-only module to return any [non-zero] data, using either an I2C or SPI configuration. The sensor is recognised on the I2C bus, and can be successfully initialised on either bus, but only zeroes are returned in response to requests for sensor data.

The ENS160 sensor on the [blue] combo board, in contrast, returns data as expected in either configuration. The AHT21 temperature sensor, however, is only accessible via the I2C interface.

Application

An ENS160 sensor will ultimately become a component of my local weather station.

Configuration

Refer to the DFRobot Wiki and DFRobot project pages. There's a lot of good stuff there and little value in my simply regurgitating any of it here.

Hardware

While my intended application for the ENS160 sensor is based on the CubeCell-Board Plus platform, both Arduino Pro Mini and NodeMCU configurations were also tested. This was primarily to eliminate the possibility that the problems experienced with the [purple] ENS160-only module were related to the processor platform—the Wire (I2C) and SPI libraries used to communicate with the sensor are platform-specific. In the event, at the sketch level and with the aid of conditional compilation macros, the code for all platforms is practically identical, the only customisable element being the relevant processor pin specifications (see below).

I2C Bus

My original intention was to use the I2C option, but this was before I discovered that there could be a conflict between the either of I2C addresses available for the ENS160 IC (0x52 or 0x53) and those potentially used by some of the AT24Cxx EEPROMs (0x50..0x57) that I had chosen to use in my Nodes. Nonetheless, the AHT21 temperature sensor included on the [blue] ENS160+AHT21 module is only accessible via the I2C interface so, if it were to be used, an I2C configuration was going to be required.

Pro Mini / ENS160 I2C Hardware Configuration Pro Mini / ENS160+AHT21 I2C Hardware Configuration

Arduino Pro Mini / ENS160 I2C Electrical Circuits

CubeCellPlus / ENS160 I2C Hardware Configuration CubeCellPlus / ENS160+AHT21 I2C Hardware Configuration

CubeCell Plus / ENS160 I2C Electrical Circuits

NodeMCU / ENS160 I2C Hardware Configuration NodeMCU / ENS160+AHT21 I2C Hardware Configuration

NodeMCU / ENS160 I2C Electrical Circuits
Pin Configurations
Arduino Pro Mini CubeCell Plus NodeMCU ENS160 ENS160 + AHT21
VIN
ACC VDD 3V3 3V3 3V3
GND GND GND GND GND
A5 SCL D1 SCL SCL
A4 SDA D2 SDA SDA
CS CS
ADD ADD
A0 GPIO11 D3 INT INT

Although not included in the above illustrations, it is generally recommended to configure pull-up resistors on the I2C SDA & SCL lines. Indeed, all of my application circuits include these pull-up resistors. Nonetheless, I have found that simple tests, such as those described herein, where connections are made with relatively short (10~20cm) wires, are generally fine without them.

SPI Bus

While, as noted above, using the SPI interface option on the ESNS160 modules avoids any potential I2C address conflicts, the main reason for testing the SPI interface was to eliminate the I2C bus interface as the cause of the problems encountered with the [purple] ENS160-only module. In the event, the results were the same regardless of the processor platform or bus used.

Pro Mini / ENS160 SPI Hardware Configuration Pro Mini / ENS160+AHT21 SPI Hardware Configuration

Arduino Pro Mini / ENS160 SPI Electrical Circuits

CubeCellPlus / ENS160 SPI Hardware Configuration CubeCellPlus / ENS160+AHT21 SPI Hardware Configuration

CubeCell Plus / ENS160 SPI Electrical Circuits

NodeMCU / ENS160 SPI Hardware Configuration NodeMCU / ENS160+AHT21 SPI Hardware Configuration

NodeMCU / ENS160 SPI Electrical Circuits
Pin Configurations
Arduino Pro Mini CubeCell Plus NodeMCU ENS160 ENS160 + AHT21
VIN
ACC VDD 3V3 3V3 3V3
GND GND GND GND GND
13 SCK1 (GPIO3) D5 SCL (SCK) SCL (SCK)
11 MOSI1 (GPIO1) D7 SDA (MOSI) SDA (MOSI)
10 GPIO12 D4 CS CS
12 MISO1 (GPIO2) D6 ADD (MISO) ADD (MISO)
A0 GPIO11 D3 INT INT

Note that only the ENS160 sensor is accessible via the SPI interfacce. The AHT21 temperature sensor, on the ENS160+AHT21 module, can only be used in an I2C configuration.

Software

When using the I2C interface, the ENS160 sensor on the [purple] ENS160-only board defaults to I2C address 0x52 on start-up while that on the ENS160+AHT21 board defaults to I2C address 0x53, with the AHT21 sensor on the latter using the I2C address 0x38. The default I2C address of the ENS160 sensor on either board can nonetheless be overridden by holding the ADD pin to GND to use 0x52 or VCC to use 0x53.

I tested both boards with three ENS160 libraries (AdaFruit, DFRobot and SparkFun) available through the Arduino IDE Library Manager but the only one that actually worked 'out of the box' with either of boards I was using was the DFRobot DFRobot_ENS160 library. Conveniently, DFRobot also had a library, DFRobot_AHT20, that could be used with the AHT21 sensor and which I reasoned might also, coming from the same developer, minimise the potential for any library conflicts when using both.

The original DFRobot_ENS160 library was nonetheless modified to add a method that allowed the 'dynamic' setting of the I2C address being used to communicate with a sensor, thus allowing me to use the same sketch with either ENS160 module. This modified version of the library is available from my GitHub repo.

The following sketch was used to run the sensors during their initial burn-in phase and more generally test their operational characteristics.


ENS160 / AHT21 Test
/*
    This is a conglomeration of the DFRobot example sketches for the ENS160 and AHT21 sensors.
    It uses a modified version of the DFRobot DFRobot_ENS160 library that includes an
    alternate begin method to allow 'dynamic' specification of the I2C bus and address
 
    Digital Concepts
    08 Sep 2025
    digitalconcepts.net.au
 */

#include <DFRobot_AHT20.h>
#include <DFRobot_ENS160.h>

/* Remove comment from definitions required for processor platform and interface being used
// Arduino Pro Mini
// I2C - SDA (A4) & SCL (A5) defined in pins_arduino.h
// SPI - MOSI (11), MISO (12) & SCK (13) defined in pins_arduino.h
#define CS		10

// NodeMCU
// I2C - SDA (D2) & SCL (D1) defined in pins_arduino.h
// SPI -  MOSI (D7), MISO (D6) & SCK (D5) defined in pins_arduino.h
#define CS		D4

// Heltec CubeCell-Board Plus
// I2C - SDA & SCL defined in pins_arduino.h
// SPI - MOSI1 (GPIO1), MISO1 (GPIO2) & SCK1 (GPIO3) defined in pins_arduino.h
#define SPI   SPI1  // CubeCell Plus uses SPI1
#define CS		GPIO4
*/

DFRobot_AHT20 aht21;

#define I2C_COMMUNICATION  // I2C bus - Comment out this statement to use SPI bus

#ifdef  I2C_COMMUNICATION
  #define ENS160_I2C_Address_1 0x52
  #define ENS160_I2C_Address_2 0x53
  DFRobot_ENS160_I2C ens160;
#else
  DFRobot_ENS160_SPI ens160(&SPI, CS);
#endif


void setup(void)
{
  Serial.begin(115200);
  while (!Serial);
  Serial.println("[setup] Initialising sensors...");
   
  float temperature = 25.0;
  float humidity = 35.0;

  /*
    Ambient temperature and humidity readings, if available, are used to calibrate
    ENS160 air quality measurements
   */

  #ifdef I2C_COMMUNICATION
    Serial.println("[setup] Using I2C bus...");
    #ifdef __ASR_Arduino__
      Wire.begin(SDA,SCL);
    #else 
      Wire.begin();
    #endif

    uint8_t status;
    if ((status = aht21.begin()) != 0) {
      Serial.print("[setup] AHT21 sensor initialization failed. Error status : ");
      Serial.println(status);
    } else {
      Serial.println("[setup] AHT21 sensor found");
    }
    if (aht21.startMeasurementReady(/* crcEn = */true)) {
      temperature = aht21.getTemperature_C();
      Serial.print("[setup] Temperature : ");
      // Get temp in Celsius (°C), range -40-80°C
      Serial.print(temperature);
      Serial.println(" °C");
      // Get temp in Fahrenheit (F)
  //    Serial.print(aht20.getTemperature_F());
  //    Serial.println(" °F");
      // Get relative humidity (%RH), range 0-100%
      humidity = aht21.getHumidity_RH();
      Serial.print("[setup] Humidity    : ");
      Serial.print(humidity);
      Serial.println(" % RH");
    } else {
      Serial.println("[setup] No temperature data available");
      Serial.println("[setup] Using default ambient conditions for calibration...");
      Serial.print("[setup] Temperature : ");
      Serial.print(temperature);
      Serial.println(" °C");
      Serial.print("[setup] Humidity    : ");
      Serial.print(humidity);
      Serial.println(" % RH");
    }

    Serial.println("[setup] Check possible ENS160 sensor addresses...");
    Serial.print("[setup] Try 0x");
    Serial.print(ENS160_I2C_Address_1,HEX);
    Serial.print("...");
    Wire.beginTransmission(ENS160_I2C_Address_1);
    if (Wire.endTransmission() == 0)  {
      Serial.println("sensor found");
      while( NO_ERR != ens160.begin(&Wire, ENS160_I2C_Address_1) ){
        Serial.println("[setup] ENS160 sensor initialisation failed...");
        delay(3000);
      }
    } else {
      Serial.println("no reponse");
      Serial.print("[setup] Try 0x");
      Serial.print(ENS160_I2C_Address_2,HEX);
      Serial.print("...");
      Wire.beginTransmission(ENS160_I2C_Address_2);
      if (Wire.endTransmission() == 0)  {
        Serial.println("sensor found");
        while( NO_ERR != ens160.begin(&Wire, ENS160_I2C_Address_2) ){
          Serial.println("[setup] ENS160 sensor initialisation failed...");
          delay(3000);
        }
      } else {
        Serial.println("no reponse");
        Serial.println( "[setup] Unable to identify ENS160 sensor" );
        while (true);
      }
    }
  #else
    Serial.println("[setup] Using SPI bus...");
    Serial.println("[setup] Using default ambient conditions for calibration...");
    Serial.print("[setup] Temperature : ");
    Serial.print(temperature);
    Serial.println(" °C");
    Serial.print("[setup] Humidity    : ");
    Serial.print(humidity);
    Serial.println(" % RH");
    while( NO_ERR != ens160.begin() ){
      Serial.println("[setup] Failed to initialise ENS160 sensor...");
      delay(3000);
    }
    Serial.println("[setup] ENS160 sensor initialised");
  #endif
  Serial.println("[setup] Set calibration variables...");
  ens160.setTempAndHum(temperature, humidity);

  /*
    Set Power Mode
    ENS160_SLEEP_MODE    : DEEP SLEEP mode (low power standby)
    ENS160_IDLE_MODE     : IDLE mode (low-power)
    ENS160_STANDARD_MODE : STANDARD Gas Sensing Modes
   */
  Serial.println("[setup] Set Power Mode...");
  ens160.setPWRMode(ENS160_STANDARD_MODE);

  Serial.println("[setup] Initialisation complete");
  Serial.println();
}

void loop()
{
  /*
    Get the Sensor Operating Status
    Return value: 0 - Normal operation, 
                  1 - Warm-Up phase, first 3 minutes after power-on.
                  2 - Initial Start-Up phase, first full hour of operation after initial power-on. Only once in the sensor’s lifetime.
    Note: The status will only be stored in the non-volatile memory after an initial 24h of continuous
          operation. If unpowered before conclusion of said period, the ENS160 will resume "Initial Start-up" mode
          after re-powering.
   */
  uint8_t Status = ens160.getENS160Status();
  Serial.print("[loop] Sensor Operating Status : ");
  Serial.println(Status);

  /*
    Get the Air Quality Index
    Return value: 1 - Excellent
                  2 - Good
                  3 - Moderate
                  4 - Poor
                  5 - Unhealthy
   */
  uint8_t AQI = ens160.getAQI();
  Serial.print("[loop]       Air Quality Index : ");
  Serial.println(AQI);

  /*
    Get Total Volatile Organic Compound (TVOC) concentration
    Return value range: 0–65000, unit: ppb
   */
  uint16_t TVOC = ens160.getTVOC();
  Serial.print("[loop]                    TVOC : ");
  Serial.print(TVOC);
  Serial.println(" ppb");

  /*
    Get CO2 equivalent concentration calculated according to the detected data of VOCs and hydrogen (eCO2 – Equivalent CO2)
    Return value range: 400–65000, unit: ppm
    Five levels:  Excellent ( 400 -  600)
                  Good      ( 600 -  800)
                  Moderate  ( 800 - 1000)
                  Poor      (1000 - 1500)
                  Unhealthy (     > 1500)
   */
  uint16_t ECO2 = ens160.getECO2();
  Serial.print("[loop]                    eCO2 : ");
  Serial.print(ECO2);
  Serial.println(" ppm");

  Serial.println();
  delay(5000);
}

While the [purple] ENS160-only board was recognised and successfully initialised when connected via either the I2C or SPI bus, I could never get it to return any data. Following comments in various forum posts on the subject, I tried using different power sources and even leaving the module running for 24 hours in case it only became active after the recommended burn-in period, all without success. Using essentially the same configurations, the ENS160+AHT21 board ran, without any problems, from the outset.

Calibration

Having now worked with the three different air quality sensors, CCS811, ENS160 and MQ-135, there is clearly a need for some level of calibration. Out of the box, the three sensors give wildly different readings and they certainly need the recommended burn-in period (48hrs) before measurements from individual sensors settle down and become consistent. After the burn-in period, the relationship between eCO2 and TVOC measurements on the individual CS811 and ENS160 sensors are at least consistent, but the absolute measurements on the CCS811 (eCO2 1980 ppm) are still more than double those measured with the ENS160 (eCO2 790 ppm) in my current test environment, and even the latter is higher than I'd expect it to be. There's still a bit of work to do here...

Sensor Readings

Refer to the ENS160 datasheet for a full description of the capabilities and usage of the sensor.

Start-Up and Response Times

The following details, taken directly from the ENS160 datasheet, are of particular interest in relation to taking measurements with the sensor.

State Max Time
Initial Start-Up 1 hour
Warm-Up 3 minutes
Immediate Response 1 second

Initial Start-Up is the time the ENS160 needs after its first ever power-on before it will return reasonable air quality readings. It is recommended that a new sensor be powered on and left running for at leat 24 hours before actually being used. Changes in raw resistance signals and sensitivities will be greatest in the first 48 hours of operation.

After the initial power-up, a conditioning or warm-up period is also generally recommended to allow the sensor to stabilise, after idle periods or power-off, before making measurements.

OPMODE

The ENS160 sensor can operate in three modes: Standard [Gas Sensing Mode], IDLE (low power) or DEEP SLEEP (low-power standby). It would seem that the normal mode of operation is for the sensor to sit in IDLE mode until it is required to make a reading, when it would be switched to STANDARD mode. Given that, in the present case, the sensor is intended to be part of a battery-powered configuration, it will likely spend most of its time in either IDLE or DEEP SLEEP mode. I am, however, yet to explore these options and any implications they might have in the way a Node's hardware/software environment needs to be managed.

Interrupts

While some sensors provide the ability to configure the generation of an interrupt when a previously set threshold is exceeded, the ENS160 interrupt model is simply one where an interrupt is generated when new data is available. This may or may not be of any interest, depending on how the sensor responds to being brought out of DEEP SLEEP when the Node cycles through a reporting period, but I am yet to explore this option.

03-09-2026