The HTU21D is a popular and affordable sensor for measuring both temperature and humidity with a microcontroller. While there are many libraries for interacting with the sensor in the arduino-ecosystem, at the time of writing this, I didn’t find a good library to support the sensor with ESP-IDF while utilizing FreeRTOS. So here is a little write-up about how I implemented a small custom driver for the HTU21D.

First things first: The specification. According to the datasheet, the sensor communicates over I2C using the address 0x40 and a speed of 100kHz.

In ESP-IDF, we can configure the I2C-bus with the i2c_config_t-struct as well as the i2c_param_config and the i2c_driver_install-functions. All the needed constants and functions are defined in these 2 headers:

#include "driver/i2c.h"
#include "driver/gpio.h"

To set everything up, we’ll define a new function, HTU21D_InitI2C(), which calls all the necessary functions and sets up all the structures that are needed.

int HTU21D_InitI2C() {
	i2c_config_t c = {
		.mode = I2C_MODE_MASTER,
		.sda_io_num = 21, // Default SDA on my board but could be any pin
		.scl_io_num = 22, // Same as SDA
		.sda_pullup_en = GPIO_PULLUP_ENABLE,
		.scl_pullup_en = GPIO_PULLUP_ENABLE,
		.master.clk_speed = 100000 // Clock speed of 100 mHz
	};
	
	// First parameter is the I2C master port, check the datasheet of your MCU to find the correct number
	int ret = i2c_param_config(1, &c); 
	if (ret != ESP_OK) {
		return 1;
	}
	
	// Argument 3 and 4 are only used in slave-mode, argument 5 is for interrupt config
	// which will not be used in this example
	ret = i2c_driver_install(1, config.mode, 0, 0, 0);
	
	return ret;
}

Because both the SDA and the SCL lines need to be pulled to VCC, we enable both internal pullup-resistors on the board. If you would design a custom PCB, these could also be on the board itself, but for a prototype, this is pretty neat.

With the setup done, we can start communicating with the sensor. To start off with, we’ll define a new function that takes a command as an argument, sends it to the sensor and returns the value it got:

uint16_t HTU21D_communicate(uint8_t command)
{
    i2c_cmd_handle_t cmd = i2c_cmd_link_create();
    i2c_master_start(cmd);
    i2c_master_write_byte(cmd, (I2C_ADDR << 1) | I2C_MASTER_WRITE, true);
    i2c_master_write_byte(cmd, command, true);
    i2c_master_stop(cmd);
    i2c_master_cmd_begin(1, cmd, 1000 / portTICK_RATE_MS);
    i2c_cmd_link_delete(cmd);

    vTaskDelay(50 / portTICK_RATE_MS);

    uint8_t msb, lsb, crc;
    cmd = i2c_cmd_link_create();
    i2c_master_start(cmd);
    i2c_master_write_byte(cmd, (I2C_ADDR << 1) | I2C_MASTER_READ, true);
    i2c_master_read_byte(cmd, &msb, 0x00);
    i2c_master_read_byte(cmd, &lsb, 0x00);
    i2c_master_read_byte(cmd, &crc, 0x01);
    i2c_master_stop(cmd);
    i2c_master_cmd_begin(1, cmd, 1000 / portTICK_RATE_MS);
    i2c_cmd_link_delete(cmd);

    uint16_t raw_value = ((uint16_t)msb << 8) | (uint16_t)lsb;
    if (!HTU21D_IsCrcValid(raw_value, crc))
        return 999;
    return raw_value & 0xFFFC;
}

First, we send the given command to the sensor by creating a so called I2C-link. After that, we give the sensor some time to react, in this case, we wait for 50ms. Last, we create a new I2C-link, write the I2C_MASTER_READ-byte (which essentially means we are ready to receive data) and then start reading 3 bytes. The first 2 bytes make up the value, the third byte is the checksum for the given value. We then stick the lsb and the msb-bytes together to form the final 16-bit value.

The algorithm for validating the checksum can be seen in the datasheet linked further above, but for simplicity, this is the function that validates it:

uint8_t HTU21D_IsCrcValid(uint16_t value, uint8_t crc)
{
    uint32_t row = (uint32_t)value << 8;
    row |= crc;

    uint32_t divisor = (uint32_t)0x988000;

    for (int i = 0; i < 16; i++)
    {
        if (row & (uint32_t)1 << (23 - i))
            row ^= divisor;
        divisor >>= 1;
    }
    
    return (row == 0);
}

Now that we covered all the necessary functions, we can start interacting with the sensor and reading the temperature and humidity:

uint16_t HTU21D_GetTemperature() {
	uint16_t raw_temperature = read_from_sensor(0xF3);
	
	// Convert the raw value to temperature in celsius (taken from the datasheet)
	return (raw_temperature * 175.72 / 65536.0) - 46.85;
}

uint16_t HTU21D_GetHumidity() {
	uint16_t raw_humidity = read_from_sensor(0xF5);
	
	// Convert the raw value to relative humidity in percentage (taken from the datasheet)
	humidity = (raw_humidity * 125.0 / 65536.0) - 6.0;
}

And that’s it. We now have a working HTU21D-driver for ESP-IDF, without relying on any Arduino- or third-party libraries except the I2C-interface that comes with ESP-IDF.