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.