htu21df-sensor 0.1.4

support for MEAS HTU21D(F) temperature + humidity sensors
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
/* SPDX-License-Identifier: MIT */

#![allow(unused)]
#![deny(unsafe_code)]
#![deny(missing_docs)]
#![cfg_attr(not(any(test, feature = "std")), no_std)]

/*!
 * # About
 *
 * This is a Rust driver for the *HTU21D(F)* temperature / humidity sensor
 * by TE MEAS found in widely available sensor components. It was tested on
 * GY-21 sensor but should work with different HTU21 based sensors too.
 *
 * The implementation is based on [the official datasheet by TE
 * MEAS](https://cdn-shop.adafruit.com/datasheets/1899_HTU21D.pdf).
 *
 * # Example performing measurements with the HTU21D on the ESP32
 *
 * In this example only the HTU21 (GY-21) is accessed over I²C but since
 * in a real-world application that is unlikely to be the only slave
 * device on the bus, the I²C struct is wrapped with the
 * [``shared-bus``](https://docs.rs/shared-bus) crate.
 *
 * ```ignore
 *  use esp_println::{print, println};
 *  use hal::{clock::ClockControl, gpio::IO, i2c::I2C, peripherals::Peripherals,
 *            prelude::*, Delay};
 *  use htu21df_sensor::Sensor;
 *
 *  // set up clock and delay handle to ensure the proper duration between
 *  // measurement initiation and reading the result
 *  let clocks = ClockControl::max(system.clock_control).freeze();
 *  let mut delay = Delay::new(&clocks);
 *
 *  // initialize I²C with the default GPIO pins on ESP32
 *  let i2c = I2C::new(
 *      peripherals.I2C0,
 *      io.pins.gpio21,
 *      io.pins.gpio22,
 *      100u32.kHz(),
 *      /* TODO: this will go away soon */
 *      &mut system.peripheral_clock_control,
 *      &clocks,
 *  );
 *
 *  // wait for the sensor to become live; the 15 are suggested in the datasheet
 *  delay.delay_ms(15);
 *
 *  // no need to own the I²C bus
 *  let bus = shared_bus::BusManagerSimple::new(i2c);
 *
 *  // finally, initialize the sensor
 *  println!("initializing HTU21D ...");
 *  let mut htu = Sensor::new(bus.acquire_i2c(), Some(&mut delay)).expect("sensor init");
 *
 *  // the sensor is now ready to use
 *  print!("measuring ...");
 *  let humidity: f32 = htu.measure_humidity(&mut delay).expect("humidity").value();
 *  let temperature: f32 = htu.measure_temperature(&mut delay).expect("temperature").value();
 *
 *  println!(" done. temperature: {} °C, rel. humidity: {} %", temperature, humidity);
 * ```
 *
 * # Implementation notes
 *
 * As of version 0.1 only blocking APIs are provided.
 */

use embedded_hal::blocking::{delay::DelayMs,
                             i2c::{Read, Write}};

#[cfg(feature = "std")] mod std;

mod constants
{
    /*
     * Below values are from the datasheet.
     */
    /** slave address of the HTU21D sensor */
    pub const I2C_ADDR: u8 = 0x40;

    /** HTU21D no-hold temperature register */
    pub const I2C_COMMAND_TEMPERATURE: u8 = 0xf3;

    /** HTU21D no-hold humidity register */
    pub const I2C_COMMAND_HUMIDITY: u8 = 0xf5;

    /** power cycle the sensor */
    pub const SOFT_RESET: u8 = 0xfe;

    /** wait before reading temperature */
    pub const TEMPERATURE_DELAY_MS: u16 = 50;

    /** wait before reading humidity */
    pub const HUMIDITY_DELAY_MS: u16 = 16;

    /**
     * Wait after resetting the sensor (power cycle and reinitialization).
     * It is guaranteed that the sensor reboots in less than 15 ms; cf. p.
     * 12 of HTU21 datasheet.
     */
    pub const RESET_DELAY_MS: u16 = 15;

    /** wait for sensor to become ready */
    pub const POWERUP_DELAY_MS: u16 = 15;

    /** The checksum uses a 9-bit polynomial of 2^8 + 2^5 + 2^4 + 1. */
    pub const CRC8_POLY: u32 = 0b100110001;
}

/**
 * Struct representing an interface to the HTU21D(F) on the I²C bus.
 */
#[derive(Debug)]
pub struct Sensor<I2C>
{
    i2c:  I2C,
    addr: u8,
}

/** Two bytes read from I²C bus representing a measurement. Values are
 * provided in big endian order. */
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct RawMeasurement
{
    lo: u8,
    hi: u8,
}

/** Two constants are required to perform a measurement: the command or register
 * address ``C`` and the delay ``D`` in ms from initiating the measurement to
 * reading the result. Both values are different for temperature and relative
 * humidity on the HTU21. */
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Measurement<Output, const D: u16, const C: u8>(Output);

/** A type defining the measurement for relative humidity. */
pub type Humidity = Measurement<
    f32,
    { constants::HUMIDITY_DELAY_MS },
    { constants::I2C_COMMAND_HUMIDITY },
>;

/** A type defining the measurement for temperature. */
pub type Temperature = Measurement<
    f32,
    { constants::TEMPERATURE_DELAY_MS },
    { constants::I2C_COMMAND_TEMPERATURE },
>;

impl<Output: Copy, const D: u16, const C: u8> Measurement<Output, D, C>
{
    const fn i2n_command(&self) -> u8 { C }

    const fn delay_ms(&self) -> u16 { D }

    /** Access the output value of a measurement. */
    pub const fn value(&self) -> Output { self.0 }
}

impl From<RawMeasurement> for u16
{
    fn from(RawMeasurement { lo, hi }: RawMeasurement) -> Self
    {
        (hi as u16) << 8 | (lo as u16 & 0xfc_u16)
    }
}

impl From<RawMeasurement> for Temperature
{
    /** 16 bits of temperature; cf. p. 15 of the datasheet. */
    fn from(raw: RawMeasurement) -> Self
    {
        let sigout: u16 = raw.into();

        /* Temp = -46.85 + 175.72 (S_Temp/2^16) */
        Self(0.002681274_f32 * (sigout as f32) - 46.85_f32)
    }
}

impl From<RawMeasurement> for Humidity
{
    /** 12 bits of humidity; cf. p. 15 of the datasheet. */
    fn from(raw: RawMeasurement) -> Self
    {
        let sigout: u16 = raw.into();

        /* RH = -6 + 125 (S_RH/2^16) */
        Self(0.001907349_f32 * (sigout as f32) - 6.0_f32)
    }
}

/** Error conditions returned from struct ``Sensor`` member functions. */
#[derive(Copy, Clone, Debug)]
pub enum Error<I2cError>
{
    /** ``I2c`` wraps I²C errors. */
    I2c(I2cError),

    /** ``Crc`` indicates a corrupt reading. */
    Crc,
}

impl<I2C, E> Sensor<I2C>
where I2C: Read<Error = E> + Write<Error = E>
{
    /**
     * Create a new struct ``Sensor`` for the given I²C interface with the
     * HTU21D listening on the default bus address. If ``delay`` is passed,
     * an initial reset is performed as recommended in the datasheet.
     *
     * Cf. p. 10 of the datasheet.
     */
    pub fn new(
        i2c: I2C,
        delay: Option<&mut impl DelayMs<u16>>,
    ) -> Result<Self, Error<E>>
    {
        let mut htu = Self { i2c, addr: constants::I2C_ADDR };

        if let Some(delay) = delay {
            delay.delay_ms(constants::POWERUP_DELAY_MS);
            htu.reset(delay)?;
        }

        Ok(htu)
    }

    /**
     * Release the owned I²C device by consuming ``self``.
     *
     * This can be useful for sharing the same bus between multiple
     * drivers manually without resorting to a manager like the
     * [``shared-bus``](https://docs.rs/shared-bus) crate.
     */
    pub fn destroy(self) -> I2C { self.i2c }

    /**
     * Create a new struct ``Sensor`` for the given I²C interface with
     * a custom bus address. If ``delay`` is passed, an initial reset is
     * performed as recommended in the datasheet.
     *
     * Use this e. g. if you’ve resolved a bus address conflict with a
     * multiplexer and the HTU21D is listening on a non-default address.
     */
    pub fn with_address(
        i2c: I2C,
        delay: Option<&mut impl DelayMs<u16>>,
        addr: u8,
    ) -> Result<Self, Error<E>>
    {
        let mut htu = Self { i2c, addr };

        if let Some(delay) = delay {
            delay.delay_ms(constants::POWERUP_DELAY_MS);
            htu.reset(delay)?;
        }

        Ok(htu)
    }

    /**
     * Issue commands to the HTU21D over I²C. ``cmd`` must be a valid command
     * as describe in the datasheet.
     */
    fn send_command<C: Into<u8>>(&mut self, cmd: C) -> Result<(), Error<E>>
    {
        let cmd: u8 = cmd.into();
        self.i2c.write(self.addr, [cmd].as_slice()).map_err(Error::I2c)
    }

    /**
     * Initiate a measurement of a particular type.
     *
     * This will return immediately after submitting the measurement command
     * over I²C without waiting for the sensor to become ready for reading
     * the result. It can be used paired with ``measurement_result()`` to
     * implement non-blocking APIs. */
    pub fn start_measurement<const C: u8>(&mut self) -> Result<(), Error<E>>
    {
        self.send_command(C)
    }

    /**
     * Read a value for which a measurement has been initiated.
     *
     * This will return immediately after reading the value from the
     * I²C without waiting for the sensor to become ready first.
     * It can be used together with ``start_measurement()`` to implement
     * non-blocking APIs.
     *
     * Note that this API provides no safeguards against conflating
     * measurement types or insufficient delay since initiating the
     * measurement. Ensuring read readiness of the sensor is up to
     * caller.
     */
    pub fn measurement_result<M>(&mut self) -> Result<M, Error<E>>
    where M: From<RawMeasurement>
    {
        let raw = self.raw_measurement_result()?;
        Ok(raw.into())
    }

    fn raw_measurement_result(&mut self) -> Result<RawMeasurement, Error<E>>
    {
        self.read_value_checked().map(|(hi, lo)| RawMeasurement { hi, lo })
    }

    /**
     * Read three bytes from I²C where the third byte is considered the CRC.
     * Returns an error if this byte does not match the CRC computed from the
     * first two bytes.
     *
     * When the CRC check succeeds, a pair holding the high and low byte is
     * returned.
     */
    fn read_value_checked(&mut self) -> Result<(u8, u8), Error<E>>
    {
        let mut buf = [0; 3];
        self.i2c.read(self.addr, &mut buf).map_err(Error::I2c)?;

        if calc_crc(buf[0], buf[1]) != buf[2] {
            Err(Error::Crc)
        } else {
            Ok((buf[0], buf[1]))
        }
    }

    /** Perform one blocking temperature measurement for the given measurement
     * type, waiting the appropriate amount of time before reading the value. */
    pub fn measure_temperature(
        &mut self,
        delay: &mut impl DelayMs<u16>,
    ) -> Result<Temperature, Error<E>>
    {
        self.measure::<f32, { constants::TEMPERATURE_DELAY_MS }, {constants::I2C_COMMAND_TEMPERATURE}>(delay)
    }

    /** Perform one blocking humidity measurement for the given measurement
     * type, waiting the appropriate amount of time before reading the value. */
    pub fn measure_humidity(
        &mut self,
        delay: &mut impl DelayMs<u16>,
    ) -> Result<Humidity, Error<E>>
    {
        self.measure::<f32, { constants::HUMIDITY_DELAY_MS }, {constants::I2C_COMMAND_HUMIDITY}>(delay)
    }

    /**
     * Send a soft reset command to the sensor and wait for the appropriate time
     * for the device to initialize itself.
     *
     * Cf. p. 12 of the datasheet.
     */
    pub fn reset(
        &mut self,
        delay: &mut impl DelayMs<u16>,
    ) -> Result<(), Error<E>>
    {
        self.send_command(constants::SOFT_RESET)?;
        delay.delay_ms(constants::RESET_DELAY_MS);
        Ok(())
    }

    /**
     * Read a sequence of bytes from a HTU21D sensor registers with the given
     * delay between write and read operations.
     */
    #[inline]
    fn measure<Output, const D: u16, const C: u8>(
        &mut self,
        delay: &mut impl DelayMs<u16>,
    ) -> Result<Measurement<Output, D, C>, Error<E>>
    where
        Measurement<Output, D, C>: From<RawMeasurement>,
    {
        self.start_measurement::<C>()?;
        delay.delay_ms(D);
        self.measurement_result::<Measurement<Output, D, C>>()
    }
}

/**
 * Compute 8-bit CRC using a 9 bit polynomial.
 *
 * Datasheet p. 14. (The description reads suspiciously similar to
 * that on the Wikipedia page:
 * https://en.wikipedia.org/wiki/Cyclic_redundancy_check#Computation )
 */
#[inline]
const fn calc_crc(hi: u8, lo: u8) -> u8
{
    let mut crc: u32 = (hi as u32) << 16 | (lo as u32) << 8;
    let mut div: u32 = constants::CRC8_POLY << 15;

    let mut b = 24usize;
    while b != 8 {
        b -= 1;
        if ((crc & (1 << b)) != 0) {
            crc ^= div;
        }
        div >>= 1;
    }

    /* The eight bits left on the right are the CRC. */
    (crc & 0xff) as u8
}

#[cfg(test)]
mod tests
{
    use super::*;

    /** Page 15 in datasheet. */
    #[test]
    fn crc()
    {
        assert_eq!(calc_crc(0b00000000u8, 0b11011100u8), 0b01111001u8);
        assert_eq!(calc_crc(0b01101000u8, 0b00111010u8), 0b01111100u8);
        assert_eq!(calc_crc(0b01001110u8, 0b10000101u8), 0b01101011u8);
    }

    /** Page 15 in datasheet. */
    #[test]
    fn humidity()
    {
        use RawMeasurement as RM;

        /* 0x7c80 ⇒ 54.8 %RH */
        assert_eq!(
            Humidity::from(RM { lo: 0x80, hi: 0x7c }),
            Measurement(54.791027)
        );
        /* 0x4e85 ⇒ 32.3 %RH */
        assert_eq!(
            Humidity::from(RM { lo: 0x85, hi: 0x4e }),
            Measurement(32.337715)
        );
    }

    /** Page 15 in datasheet. */
    #[test]
    fn temp()
    {
        use RawMeasurement as RM;
        /* 0x683a ⇒ 24.7 °C */
        assert_eq!(
            Temperature::from(RM { lo: 0x3a, hi: 0x68 }),
            Measurement(24.686394)
        );
    }
}