Embedded HandbookApps
All sections › VII. Documentation and its language
STM32F103, STM32F407 · level intermediate · 14 min read
Open in the interactive handbook

39в. From a third-party chip's datasheet to a driver: HTS221 from the first page to testing without a board

Sections 39–39б taught you to read ST documents for the microcontroller itself. But half of the work with a board is third-party chips: sensors, memory chips, converters. They have their own datasheet, and you usually write the driver for them yourself. Here is the whole path on a single example — the HTS221 humidity and temperature sensor: what to read in the datasheet and in what order, what to write down from it, how to structure the driver so that it works both on the F103 without floating point and on the F407 with it, and how to test it without a board — against a model of the sensor on a PC.

Sources: the HTS221 datasheet DocID026333 Rev 5 (March 2023) from st.com. For cross-checking — the ST driver hts221_reg.h (BSD-3) and the Zephyr driver (Apache-2.0) from the Zephyr tree on disk: their text is not copied; addresses, bits and formulas were checked against them by a script. The driver and the model are our own, built with armclang and gcc.

Terms used in the documentation

Term Meaning
datasheet the technical description of a part
pin description what each pin is for
absolute maximum ratings limits that must never be exceeded
register address map the table of registers and their addresses
slave address, SAD the device address on the bus
sub-address, SUB the register address inside the chip
auto-increment the register address advances by itself
block data update, BDU the output is updated as a block (both bytes from one measurement)
one-shot a single measurement on request
power-down sleep mode
factory calibration calibration done at the factory
not recommended for new design do not use in new products

39в.1 The datasheet in order: what to write down

The first page is not an advertisement but a "take it or not" decision. For the HTS221 it says right away: the product is still in production, but it is not recommended for new designs, and a replacement is named. For a learning driver this does not matter; for a new product it is a reason to stop. After that, read the datasheet in this order and write down the following:

Datasheet section What to write down for the driver
Features, Tables 3–4 supply 1.7…3.6 V; 2 µA at 1 Hz, 0.5 µA in power-down; sensitivity 64 LSB per °C and 256 per %rH
Tables 2, 8; Section 4 pins; CS to the supply or floating — I²C; 100 nF capacitor at the supply; pull-ups on SCL and SDA
Table 7 supply limit 4.8 V
Table 6, Section 5.1 I²C up to 400 kHz; address; how to read several bytes in a row
Table 15 register map and reset values; what must not be touched
Section 7 control bits, ready flags
Section 8, Table 19 where the calibration lies and how to get °C and %rH from it

Also write down what the datasheet does not contain: the one-shot measurement time and the wake-up time are not given here. So the driver does not wait for a fixed delay but for the ready flags — with a timeout.


39в.2 Pins, bus and address

Six pins: supply, ground, SCL, SDA, DRDY (data ready) and CS. CS selects the interface: pulled up to the supply or left floating — the chip works in I²C (there is an internal pull-up); tied to ground — SPI. Over I²C the sensor works in standard and fast mode, up to 400 kHz.

The datasheet gives the address in 8-bit form: BEh for write and BFh for read (Table 10). The seven-bit address is 5Fh; which form the bus function expects you have to check in its description (section 17.2).

Reading a register follows Table 13: start, address with the write bit, register number, repeated start, address with the read bit, data, stop. The register number takes 7 bits, and the most significant bit enables auto-increment: if it is 1, the sensor moves to the next register by itself, and several bytes are read in one transaction (Table 14). That is why the output is read from address A8h (28h plus bit 7), and the calibration from B0h.


39в.3 Register map and control

Register Address After power-up What it holds
WHO_AM_I 0Fh BCh chip ID
AV_CONF 10h 1Bh how many samples to average
CTRL_REG1 20h 00h PD (bit 7), BDU (bit 2), ODR (bits 1:0)
CTRL_REG2 21h 00h BOOT (7), Heater (1), ONE_SHOT (0)
CTRL_REG3 22h 00h DRDY pin
STATUS_REG 27h 00h H_DA (bit 1), T_DA (bit 0)
HUMIDITY_OUT_L/H, TEMP_OUT_L/H 28h…2Bh — samples, 16-bit signed
CALIB_0…F 30h…3Fh — calibration, do not write

After power-up the sensor is in power-down: PD = 0. ODR = 00 is one-shot mode: writing 1 to ONE_SHOT starts one measurement; when it is finished, H_DA and T_DA are set in STATUS, and ONE_SHOT clears itself. H_DA is cleared when HUMIDITY_OUT_H is read, T_DA when TEMP_OUT_H is read. BDU = 1 prevents the sensor from updating the output until both bytes have been read.

The strict wording of the datasheet is worth writing down with its meaning intact: writing to registers marked Reserved can permanently damage the chip, and the calibration in 30h…3Fh must not be changed. The heater (Heater) draws 33 mA at 3.3 V — thousands of times the operating current. While it is on, the outputs must not be read: valid data comes only after the heater is switched off.

AV_CONF defaults to 1Bh: 16 temperature samples and 32 humidity samples per value; at 1 Hz this gives 2.10 µA and noise of 0.03 °C and 0.15 %rH.


39в.4 Calibration: 16 bytes and two straight lines

Every sensor is calibrated at the factory at two points, and the points are stored in the sensor itself (Section 8, Table 19): for temperature, T0 and T1 in degrees and the ADC counts T0_OUT and T1_OUT at these points; for humidity, H0, H1 and H0_T0_OUT, H1_T0_OUT. Temperature and humidity are a linear interpolation between the points.

Map of the HTS221 calibration registers from 30h to 3Fh: H0_rH_x2, H1_rH_x2, T0_degC_x8, T1_degC_x8, reserved, upper bits of T1 and T0, H0_T0_OUT, reserved, H1_T0_OUT, T0_OUT, T1_OUT with the model's bytes, reading in one transaction from B0h and assembling T0 = 10.125 °C, T1 = 34.875 °C, H0 = 33.5 %rH, H1 = 73 %rH, and the interpolation formulas

Three subtleties that make drivers report wrong values:

The datasheet gives one example for each line: T0_OUT = 300 at 80/8 = 10 °C, T1_OUT = 500 at 160/8 = 20 °C, a count of 400 gives 15.0 °C; H0_T0_OUT = 4000h at 40/2 = 20 %rH, H1_T0_OUT = 6000h at 80/2 = 40 %rH, a count of 5000h gives 30.0 %rH. Both driver variants give exactly 15.0000 and 30.0000 on these numbers.


39в.5 The driver: bus through pointers, errors, two ways of computing

The driver does not know which chip it runs on: the bus is given to it as three pointers to functions — read, write and delay — as in the ST drivers. On the F4 these are three lines around HAL (the address in HAL is 8-bit, hence the shift):

static int bus_read(void *h, uint8_t addr7, uint8_t sub, uint8_t *d, uint16_t n)
{
    return HAL_I2C_Mem_Read(h, (uint16_t)(addr7 << 1), sub, I2C_MEMADD_SIZE_8BIT, d, n, 10) != HAL_OK;
}

Error codes: HTS_EBUS (−1, no acknowledge on the bus), HTS_EID (−2, wrong WHO_AM_I), HTS_ETIMEOUT (−3, the measurement did not finish), HTS_ECAL (−4, calibration with identical points — division by zero is not allowed). Initialization checks WHO_AM_I, reads the 16 calibration bytes in one transaction and writes 84h to CTRL_REG1: PD = 1, BDU = 1, one-shot mode. The measurement:

    for (uint32_t ms = 0;; ms++) {
        if (d->bus.read(d->bus.handle, HTS221_ADDR7, HTS221_STATUS_REG, &st, 1))
            return HTS_EBUS;
        if ((st & ready) == ready)
            break;
        if (ms >= timeout_ms)
            return HTS_ETIMEOUT;
        d->bus.delay_ms(d->bus.handle, 1);
    }
    if (d->bus.read(d->bus.handle, HTS221_ADDR7, HTS221_HUMIDITY_OUT_L | HTS221_AUTOINC, b, 4))
        return HTS_EBUS;

The computation comes in two variants. For the F103 without a floating-point unit (-DHTS_FIXED) — integers: the result is in hundredths of °C and %rH, the intermediate product in 64 bits:

int32_t hts_temp_centi(const hts_calib *c, int16_t t_out)
{
    int64_t den = (int64_t)c->t1_out - c->t0_out, num;
    num = ((int64_t)c->t0_x8 * den + ((int64_t)c->t1_x8 - c->t0_x8) * ((int64_t)t_out - c->t0_out)) * 100;
    if (den < 0) { num = -num; den = -den; }
    return div_round(num, 8 * den);
}

For the F407 — the same in float. In both variants humidity is clamped to 0…100 %: the datasheet explicitly requires clamping out-of-range output in software.

armclang builds the driver without warnings: for the F103 (Cortex-M3, integer variant) — 626 bytes of code, and the only library function needed is the 64-bit unsigned __aeabi_uldivmod, not a single floating-point function; for the F407 (Cortex-M4 with FPU, float) — 578 bytes. The bus module on the F4 HAL builds there as well.


39в.6 Testing without a board

The HTS221 model on the PC is written from the same datasheet: the register map with reset values, auto-increment by bit 7, ONE_SHOT, the STATUS flags, flags cleared by reading the high bytes, BDU, no reply to a foreign address. The model's calibration is set in physical units (T0 = 10.125 °C, T1 = 34.875 °C, H0 = 33.5 %rH, H1 = 73 %rH, counts −1300, 284, −3500, 6612 — 64 LSB per degree and 256 per percent, as in Table 3) and encoded into bytes per Table 19 independently of the parsing in the driver; in the reserved bytes and the upper nibble of 35h the model puts ones to test the masks.

The driver talks to the model through a fake bus. Results:

The comparison with other drivers is done by a script. From ST's hts221_reg.h the check extracts all register addresses, the code BCh, the address BFh and the layout of the bit fields of CTRL_REG1, CTRL_REG2, STATUS_REG and 35h — everything matches our own definitions. From the Zephyr driver it extracts the expressions that parse the calibration and convert the values: on the same 16 bytes they give the same T0, T1, H0, H1 and counts. But the conversion there is integer, in eighths of a degree and halves of a percent: on this calibration it falls behind the exact line by up to 0.11 °C and 0.5 %rH — noticeable for a thermometer, irrelevant for a driver that only needs "dry or damp".


39в.7 Pitfalls

The temperature jumps by 4 °C. Without BDU the low and high bytes can come from different measurements. In the model the output changes from 00FFh to 0100h between the byte reads: without BDU the driver reads 01FFh = 511 LSB, with BDU an honest 00FFh = 255.

All four bytes are the same. Bit 7 in the register address was forgotten: without auto-increment four reads from 28h return the same byte four times (in the model — D4 D4 D4 D4). Calibration read this way turns into garbage: T0 = 835/8 °C.

−40 °C shows as +984 °C. The count was read as unsigned: −4508 turns into 61028. All counts and calibration points are int16_t.

Temperature 8 times too high, humidity 2 times. The division of T0/T1 by 8 and of H0/H1 by 2 was forgotten. And if the two high bits of T0/T1 in 35h are forgotten, 34.875 °C turns into 2.875 °C.

The sensor does not measure. PD = 0 after power-up: until PD = 1 is written, ONE_SHOT starts nothing. Do not use a delay instead of waiting for the flags — the measurement time is not given in the datasheet.

Address 5Fh "does not respond". The bus function expects the 8-bit address BEh, and it was given the 7-bit one, or the other way round.

Writing to Reserved. The datasheet warns of permanent damage; the driver writes only to CTRL_REG1 and CTRL_REG2.

A typo in the datasheet. In Table 19 the least significant bit of register 30h is labelled H0.1 — the same as its neighbour; it is H0.0, and the byte is used as a whole.


39в.8 Summary

Full source code

hts221_drv.c — the HTS221 driver written from the datasheet.

/* hts221_drv.c — раздел 39в: драйвер HTS221 по даташиту DS10866 Rev 5. */
#include "hts221_drv.h"

_Static_assert(HTS221_ADDR7 == (0xBEu >> 1), "BEh/BFh — это 7-битный адрес 5Fh со сдвигом");
_Static_assert((HTS221_CTRL1_PD | HTS221_CTRL1_BDU) == 0x84u, "PD (бит 7) и BDU (бит 2), ODR = 00");
_Static_assert((HTS221_HUMIDITY_OUT_L | HTS221_AUTOINC) == 0xA8u && (HTS221_CALIB_0 | HTS221_AUTOINC) == 0xB0u, "автоинкремент — бит 7");
_Static_assert(0x3Fu - HTS221_CALIB_0 + 1u == 16u, "калибровка — 16 регистров 30h…3Fh");

void hts_decode_calib(hts_calib *c, const uint8_t b[16])      /* b[0] — регистр 30h */
{
    c->h0_x2 = b[0];                                           /* 30h */
    c->h1_x2 = b[1];                                           /* 31h */
    c->t0_x8 = (uint16_t)(b[2] | (b[5] & 0x03u) << 8);         /* 32h и биты 1:0 регистра 35h */
    c->t1_x8 = (uint16_t)(b[3] | (b[5] & 0x0Cu) << 6);         /* 33h и биты 3:2 регистра 35h */
    c->h0_out = (int16_t)(uint16_t)(b[6] | b[7] << 8);         /* 36h, 37h — со знаком */
    c->h1_out = (int16_t)(uint16_t)(b[10] | b[11] << 8);       /* 3Ah, 3Bh */
    c->t0_out = (int16_t)(uint16_t)(b[12] | b[13] << 8);       /* 3Ch, 3Dh */
    c->t1_out = (int16_t)(uint16_t)(b[14] | b[15] << 8);       /* 3Eh, 3Fh */
}

int hts_init(hts221 *d)
{
    uint8_t id = 0, cal[16], v;
    if (d->bus.read(d->bus.handle, HTS221_ADDR7, HTS221_WHO_AM_I, &id, 1))
        return HTS_EBUS;
    if (id != HTS221_ID)
        return HTS_EID;
    if (d->bus.read(d->bus.handle, HTS221_ADDR7, HTS221_CALIB_0 | HTS221_AUTOINC, cal, 16))
        return HTS_EBUS;
    hts_decode_calib(&d->cal, cal);
    if (d->cal.t1_out == d->cal.t0_out || d->cal.h1_out == d->cal.h0_out)
        return HTS_ECAL;
    v = HTS221_CTRL1_PD | HTS221_CTRL1_BDU;                    /* ODR = 00: однократный режим */
    if (d->bus.write(d->bus.handle, HTS221_ADDR7, HTS221_CTRL_REG1, &v, 1))
        return HTS_EBUS;
    return HTS_OK;
}

int hts_measure_raw(hts221 *d, int16_t *h_out, int16_t *t_out, uint32_t timeout_ms)
{
    const uint8_t ready = HTS221_STATUS_H_DA | HTS221_STATUS_T_DA;
    uint8_t v = HTS221_CTRL2_ONE_SHOT, st = 0, b[4];
    if (d->bus.write(d->bus.handle, HTS221_ADDR7, HTS221_CTRL_REG2, &v, 1))
        return HTS_EBUS;
    for (uint32_t ms = 0;; ms++) {
        if (d->bus.read(d->bus.handle, HTS221_ADDR7, HTS221_STATUS_REG, &st, 1))
            return HTS_EBUS;
        if ((st & ready) == ready)
            break;
        if (ms >= timeout_ms)
            return HTS_ETIMEOUT;
        d->bus.delay_ms(d->bus.handle, 1);
    }
    if (d->bus.read(d->bus.handle, HTS221_ADDR7, HTS221_HUMIDITY_OUT_L | HTS221_AUTOINC, b, 4))
        return HTS_EBUS;
    *h_out = (int16_t)(uint16_t)(b[0] | b[1] << 8);
    *t_out = (int16_t)(uint16_t)(b[2] | b[3] << 8);
    return HTS_OK;
}

#ifdef HTS_FIXED
/* n / d с округлением к ближайшему, d > 0 */
static int32_t div_round(int64_t n, int64_t d)
{
    return (int32_t)(n >= 0 ? (n + d / 2) / d : -((-n + d / 2) / d));
}

/* T = (T0·8 + (T1·8 − T0·8)·(T_OUT − T0_OUT)/(T1_OUT − T0_OUT)) / 8, в сотых °C */
int32_t hts_temp_centi(const hts_calib *c, int16_t t_out)
{
    int64_t den = (int64_t)c->t1_out - c->t0_out, num;
    num = ((int64_t)c->t0_x8 * den + ((int64_t)c->t1_x8 - c->t0_x8) * ((int64_t)t_out - c->t0_out)) * 100;
    if (den < 0) { num = -num; den = -den; }
    return div_round(num, 8 * den);
}

/* RH = (H0·2 + (H1·2 − H0·2)·(H_OUT − H0_OUT)/(H1_OUT − H0_OUT)) / 2, в сотых %, обрезка 0…100 % */
int32_t hts_hum_centi(const hts_calib *c, int16_t h_out)
{
    int64_t den = (int64_t)c->h1_out - c->h0_out, num;
    int32_t rh;
    num = ((int64_t)c->h0_x2 * den + ((int64_t)c->h1_x2 - c->h0_x2) * ((int64_t)h_out - c->h0_out)) * 100;
    if (den < 0) { num = -num; den = -den; }
    rh = div_round(num, 2 * den);
    return rh < 0 ? 0 : rh > 10000 ? 10000 : rh;
}
#else
float hts_temp_c(const hts_calib *c, int16_t t_out)
{
    float k = (float)(t_out - c->t0_out) / (float)(c->t1_out - c->t0_out);
    return ((float)c->t0_x8 + ((float)c->t1_x8 - (float)c->t0_x8) * k) / 8.0f;
}

float hts_hum_rh(const hts_calib *c, int16_t h_out)
{
    float k = (float)(h_out - c->h0_out) / (float)(c->h1_out - c->h0_out);
    float rh = ((float)c->h0_x2 + ((float)c->h1_x2 - (float)c->h0_x2) * k) / 2.0f;
    return rh < 0.0f ? 0.0f : rh > 100.0f ? 100.0f : rh;
}
#endif

Next: section 40 "English–Russian correspondences by topic".

Open in the interactive handbook