Interface library for the Sensirion SHT7x series humidity and temperature sensors.

This was developed and tested on the LPC1768 board.

Currently, it doesn't work for LPC11U24. I'm investigating why.

Revision:
0:f1a93e55feb5
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sht7x.cpp	Sun Aug 11 18:11:05 2013 +0000
@@ -0,0 +1,663 @@
+// Copyright (c) 2013 Jacob Bramley
+// 
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+#define __STC_LIMIT_MACROS
+#include <stdint.h>
+#include <stddef.h>
+#include <new>
+#include <math.h>
+#include <limits>
+#include "mbed.h"
+#include "sht7x.h"
+
+// For some reason, Mbed's stdint.h doesn't define INT64_MIN.
+#ifndef INT64_MIN
+#define INT64_MIN (-0x8000000000000000)
+#endif
+
+#ifdef TARGET_LPC11U24
+#define SLEEP() sleep()
+#else
+#define SLEEP() /* Busy-wait */
+#endif
+
+namespace sht7x {
+
+// ==== Public interface. ====
+
+SHT7x::SHT7x(PinName sck, PinName sda)
+    : sck_(DigitalOut(sck)), sda_(DigitalInOut(sda)) {
+  sck_state_ = PIN_INVALID;
+  sda_state_ = PIN_INVALID;
+  state_ = STATE_UNKNOWN;
+
+  // The default sensor configuration, according to the datasheet.
+  status_.precision = PRECISION_HIGH;
+  status_.otp = OTP_ON;
+  status_.heater = HEATER_OFF;
+  status_.battery = BATTERY_GT_2_47;
+
+  // Configure the pins, but do nothing else at this point.
+  sda_.mode(OpenDrain);
+  sda_.output();
+  this->set_sda(PIN_FREE);
+  this->set_sck(PIN_0);
+}
+
+
+bool SHT7x::reset() {
+  if (!check_state(STATE_UNKNOWN, STATE_SLEEP, STATE_COMMS_ERROR)) {
+    return false;
+  }
+  state_ = STATE_RESETTING;
+
+  // If we're trying to reset the sensor, it's likely that the communications
+  // channel has got into a weird state. We need it up and running in order to
+  // send the reset command.
+  reset_comms();
+
+  command(CMD_RESET);
+  state_ = STATE_UNKNOWN;
+
+  status_.precision = PRECISION_HIGH;
+  status_.otp = OTP_ON;
+  status_.heater = HEATER_OFF;
+  status_.battery = BATTERY_GT_2_47;
+
+  return true;
+}
+
+
+bool SHT7x::initialize() {
+  if (!check_state(STATE_UNKNOWN)) {
+    return false;
+  }
+  state_ = STATE_INITIALIZING;
+
+  // Wait for the sensor to turn on.
+  // There is no external notification that the sensor is ready, so a timer
+  // is the only option here.
+  wait_us(time_reset());
+
+  state_ = STATE_SLEEP;
+
+  // Make sure that the cached status byte is up to date.
+  return status();
+}
+
+
+bool SHT7x::configure(Precision precision, OTP otp, Heater heater) {
+  if (!check_state(STATE_SLEEP)) {
+    return false;
+  }
+  state_ = STATE_SETTING_CONFIGURATION;
+
+  // Overlay the new status on the existing one.
+  if (precision != PRECISION_KEEP)  { status_.precision = precision; }
+  if (otp != OTP_KEEP)              { status_.otp = otp; }
+  if (heater != HEATER_KEEP)        { status_.heater = heater; }
+
+  // Write the status byte.
+  command(CMD_WRITE_STATUS);
+  put_byte(encode_status_byte());
+  get_ack();
+
+  // TODO: It's not obvious how to get a CRC byte from this command. Is it
+  // necessary to read the status back again?
+
+  state_ = STATE_SLEEP;
+  return true;
+}
+
+
+bool SHT7x::status() {
+  if (!check_state(STATE_SLEEP)) {
+    return false;
+  }
+
+  // Read the status register to populate the cached values.
+  command(CMD_READ_STATUS);
+  uint32_t raw = get_byte();
+  put_ack(0);
+  uint32_t crc = get_byte();
+  put_ack(1);
+
+  // We must decode the status byte before checking the CRC because the status
+  // register is used to initialize the CRC check.
+  decode_status_byte(raw);
+
+  if (check_crc(CMD_READ_STATUS, raw, crc)) {
+    state_ = STATE_SLEEP;
+  } else {
+    state_ = STATE_COMMS_ERROR;
+  }
+
+  return true;
+}
+
+
+bool SHT7x::status(Status * & status) {
+  if (this->status()) {
+    status = &status_;
+    return true;
+  }
+  return false;
+}
+
+
+bool SHT7x::measure(Temp & temp, uint32_t mvdd) {
+  if (!check_state(STATE_SLEEP)) {
+    return false;
+  }
+  state_ = STATE_MEASURING_TEMPERATURE;
+
+  // TODO: Wait for sda low, rather than a timer.
+  command(CMD_READ_TEMP);
+  wait_us(time_reading_temperature());
+
+  uint32_t raw = get_byte() << 8;
+  put_ack(0);
+  raw |= get_byte() << 0;
+  put_ack(0);
+  uint32_t crc = get_byte();
+  put_ack(1);
+
+  if (check_crc(CMD_READ_TEMP, raw, crc)) {
+    new (&temp) Temp(bits_required_temperature(), raw, mvdd);
+    state_ = STATE_SLEEP;
+
+    // Make sure that the cached status byte is up to date.
+    // In particular, the Vdd voltage detection is updated after a measurement.
+    return status();
+  }
+
+  new (&temp) Temp();
+  state_ = STATE_COMMS_ERROR;
+  return false;
+}
+
+
+bool SHT7x::measure(Hum & hum, Temp const * temp) {
+  if (!check_state(STATE_SLEEP)) {
+    return false;
+  }
+  state_ = STATE_MEASURING_HUMIDITY;
+
+  // TODO: Wait for sda low, rather than a timer.
+  command(CMD_READ_HUM);
+  wait_us(time_reading_humidity());
+
+  // Two bytes are sent even for 8-bit humidity readings.
+  uint32_t raw = get_byte() << 8;
+  put_ack(0);
+  raw |= get_byte() << 0;
+  put_ack(0);
+  uint32_t crc = get_byte();
+  put_ack(1);
+
+  if (check_crc(CMD_READ_HUM, raw, crc)) {
+    new (&hum) Hum(bits_required_humidity(), raw, temp);
+    state_ = STATE_SLEEP;
+
+    // Make sure that the cached status byte is up to date.
+    // In particular, the Vdd voltage detection is updated after a measurement.
+    return status();
+  }
+
+  new (&hum) Hum();
+  state_ = STATE_COMMS_ERROR;
+  return false;
+}
+
+
+// ==== Internal methods. ====
+
+// Construct a status register value, suitable for writing with
+// CMD_WRITE_STATUS, from cached values.
+uint32_t SHT7x::encode_status_byte() const {
+  uint8_t value = 0;
+  value |= (status_.precision == PRECISION_LOW  ) ? (0x01) : (0x00);
+  value |= (status_.otp       == OTP_OFF        ) ? (0x02) : (0x00);
+  value |= (status_.heater    == HEATER_ON      ) ? (0x04) : (0x00);
+  value |= (status_.battery   == BATTERY_LT_2_47) ? (0x40) : (0x00);
+  return value;
+}
+
+
+// Update the cached status values with a status register value obtained from
+// the sensor using CMD_READ_STATUS.
+void SHT7x::decode_status_byte(uint32_t value) {
+  status_.precision = (value & 0x01) ? PRECISION_LOW   : PRECISION_HIGH;
+  status_.otp       = (value & 0x02) ? OTP_OFF         : OTP_ON;
+  status_.heater    = (value & 0x04) ? HEATER_ON       : HEATER_OFF;
+  status_.battery   = (value & 0x40) ? BATTERY_LT_2_47 : BATTERY_GT_2_47;
+}
+
+
+inline uint32_t SHT7x::bits_required_temperature() const {
+  switch (status_.precision) {
+    default:
+    case PRECISION_HIGH:  return 14;
+    case PRECISION_LOW:   return 12;
+  }
+}
+
+
+inline uint32_t SHT7x::bits_required_humidity() const {
+  switch (status_.precision) {
+    default:
+    case PRECISION_HIGH:  return 12;
+    case PRECISION_LOW:   return 8;
+  }
+}
+
+
+inline uint32_t SHT7x::time_atom() const {
+  return 1;
+}
+
+
+inline uint32_t SHT7x::time_reset() const {
+  return 11000;
+}
+
+
+inline uint32_t SHT7x::time_reading_temperature() const {
+  switch (status_.precision) {
+    default:
+    case PRECISION_HIGH:  return 320000;  // 320ms, 14 bits
+    case PRECISION_LOW:   return 80000;   //  80ms, 12 bits
+  }
+}
+
+
+inline uint32_t SHT7x::time_reading_humidity() const {
+  switch (status_.precision) {
+    default:
+    case PRECISION_HIGH:  return 80000;   //  80ms, 12 bits
+    case PRECISION_LOW:   return 20000;   //  20ms, 8 bits
+  }
+}
+
+
+void SHT7x::start() {
+  uint32_t const t = time_atom();
+
+  set_sda(PIN_1); wait_us(t);
+  set_sck(PIN_1); wait_us(t);
+  set_sda(PIN_0); wait_us(t);
+  set_sck(PIN_0); wait_us(t);
+                  wait_us(t);
+  set_sck(PIN_1); wait_us(t);
+  set_sda(PIN_1); wait_us(t);
+  set_sck(PIN_0); wait_us(t);
+}
+
+
+void SHT7x::put_bit(int b) {
+  uint32_t const t = time_atom();
+
+  set_sda(b);     wait_us(t);
+  set_sck(PIN_1); wait_us(t);
+                  wait_us(t);
+  set_sck(PIN_0); wait_us(t);
+}
+
+
+int SHT7x::get_bit() {
+  uint32_t const t = time_atom();
+
+  set_sda(PIN_FREE); wait_us(t);
+  set_sck(PIN_1);    wait_us(t);
+  int b = sda_;      wait_us(t);
+  set_sck(PIN_0);    wait_us(t);
+
+  return b;
+}
+
+
+// The protocol uses three different ACKs: One input ACK and two output ACKs. A
+// high output ACK usually ends a transmission. A low output ACK usually
+// acknowledges a byte as part of a transmission. Refer to the SHT7x datasheet
+// for details.
+void SHT7x::put_ack(int b) {
+  uint32_t const t = time_atom();
+
+  set_sda(b);        wait_us(t);
+  set_sck(PIN_1);    wait_us(t);
+                     wait_us(t);
+  set_sck(PIN_0);    wait_us(t);
+  set_sda(PIN_FREE); wait_us(t);
+}
+
+
+void SHT7x::get_ack() {
+  uint32_t const t = time_atom();
+
+  set_sda(PIN_FREE); wait_us(t);
+  set_sck(PIN_1);    wait_us(t);
+                     wait_us(t);
+  set_sck(PIN_0);    wait_us(t);
+}
+
+
+void SHT7x::put_byte(uint32_t byte) {
+  // TODO: Unroll this.
+  for (uint32_t i = 0; i < 8; i++) {
+    put_bit((byte >> (7-i)) & 1);
+  }
+}
+
+
+uint32_t SHT7x::get_byte() {
+  uint32_t byte = 0;
+  // TODO: Unroll this.
+  for (uint32_t i = 0; i < 8; i++) {
+    byte = (byte << 1) | get_bit();
+  }
+  return byte;
+}
+
+
+inline int SHT7x::get_sda() {
+  return sda_.read();
+}
+
+
+inline void SHT7x::set_sda(int pin) {
+  set_sda((pin) ? (PIN_1) : (PIN_0));
+}
+
+
+inline void SHT7x::set_sda(pin_State pin) {
+  if (sda_state_ != pin) {
+    sda_ = (pin == PIN_0) ? (0) : (1);
+    sda_state_ = pin;
+  }
+}
+
+
+inline void SHT7x::set_sck(int pin) {
+  set_sck((pin) ? (PIN_1) : (PIN_0));
+}
+
+
+inline void SHT7x::set_sck(pin_State pin) {
+  if (sck_state_ != pin) {
+    sck_ = (pin == PIN_0) ? (0) : (1);
+    sck_state_ = pin;
+  }
+}
+
+
+void SHT7x::reset_comms() {
+  for (uint32_t i = 0; i < 9; i++) {
+    put_bit(1);
+  }
+}
+
+
+void SHT7x::command(Command command) {
+  start();
+  put_byte(static_cast<uint32_t>(command));
+  get_ack();
+}
+
+
+bool SHT7x::check_state(State s0) {
+  State state = state_;
+  return state == s0;
+}
+
+
+bool SHT7x::check_state(State s0, State s1) {
+  State state = state_;
+  return (state == s0) || (state == s1);
+}
+
+
+bool SHT7x::check_state(State s0, State s1, State s2) {
+  State state = state_;
+  return (state == s0) || (state == s1) || (state == s2);
+}
+
+
+int SHT7x::payload_size(Command command) {
+  switch (command) {
+    case CMD_READ_TEMP:
+    case CMD_READ_HUM: return 2;
+    case CMD_READ_STATUS:
+    case CMD_WRITE_STATUS: return 1;
+    case CMD_RESET:
+    default: return 0;
+  }
+}
+
+
+bool SHT7x::check_crc(Command command, uint32_t data, uint32_t crc_in) {
+  int const data_bits = payload_size(command) * 8;
+  int const bits = data_bits + 8;
+
+  data |= command << data_bits;
+
+  // The Sensirion application note describes an algorithm which reverses the
+  // bits in the status byte to form the initial CRC, performs the CRC
+  // calculation, then reverses the bits again at the end. This CRC
+  // implementation operates in reverse, so there is no need to reverse the CRC.
+
+  // TODO: Optimize this further, if possible.
+
+  // The CRC is initialized with the status byte.
+  uint32_t crc = encode_status_byte();
+  for (int bit = bits - 1; bit >= 0; bit--) {
+    // Eor bit 0 of the CRC with the top bit of the data to determine the value
+    // to feed back into the CRC.
+    uint32_t result = ((data >> bit) ^ crc) & 1;
+    // Bit 0 contains that value that must be fed back into the CRC at bit
+    // positions 7, 3 and 2, so use it to construct a bit mask. These bits
+    // correspond to positions 0, 4 and 5 in the reversed-CRC implementation.
+    uint32_t invert = (result << 7) | (result << 3) | (result << 2);
+    crc = (crc >> 1) ^ invert;
+  }
+
+  return crc == crc_in;
+}
+
+
+// ==== Readings. ====
+
+// Shift 'v' right by 'shift' and perform signed rounding.
+static inline int64_t shift_round(int64_t v, uint32_t shift) {
+  uint64_t u = static_cast<uint64_t>(v);
+
+  if (shift == 0) {
+    return v;
+  }
+
+  // Rounding (with ties away from zero).
+  // Positive:
+  //  i += bit(shift-1)
+  // Negative:
+  //  i += bit(shift-1) - 1
+  // We can simply take the value of bit 31 and subtract the value of the
+  // sign bit, then add the result to the truncated reading.
+  // Note that this relies on C99-style division, where negative values are
+  // truncated towards 0.
+  int64_t i = (v / (1LL << shift));
+  int64_t s = (u >> 63) & 1;
+  int64_t h = (u >> (shift - 1)) & 1;
+
+  return i + h - s;
+}
+
+
+// ---- Temperature ----
+
+// -- Floating point. --
+
+float Temp::get_d1_f() const {
+  // The following data-points are provided in the datasheet:
+  //  Vdd     d1
+  //  5       -40.1
+  //  4       -39.8
+  //  3.5     -39.7
+  //  3       -39.6
+  //  2.5     -39.4
+  // The relationship between Vdd and d1 is not linear. However, the
+  // deviation from a linear relationship is probably a result of aliasing
+  // rather than anything else. The following relationship provides a linear
+  // response that looks like a reasonable best-fit for the specified points:
+  //  Vdd     d1
+  //  5       -40.1
+  //  2.5     -39.43
+  //
+  // d1 = -0.268 * Vdd - 38.76
+  return -0.000268 * mvdd - 38.76;
+}
+
+
+float Temp::get_f() const {
+  float d1 = get_d1_f();
+  float d2;
+  switch (get_bits()) {
+    case 12:  d2 = 0.04f;     break;
+    case 14:  d2 = 0.01f;     break;
+    default:  return std::numeric_limits<float>::quiet_NaN();
+  }
+  return d1 + d2 * get_raw();
+}
+
+
+// -- Fixed point. --
+
+int64_t Temp::get_d1_q32() const {
+  // See notes in get_d1_f() for justification of this formula.
+  // d1 = -0.268 * Vdd - 38.76
+  return -0x0011904bLL * mvdd - 0x26c28f5c28LL;
+}
+
+
+int64_t Temp::get_q32() const {
+  int64_t d1 = get_d1_q32();
+  int64_t d2;
+  switch (get_bits()) {
+    case 12:  d2 = 0xa3d70a3LL;   break;  // 0.04, q32
+    case 14:  d2 = 0x28f5c28LL;   break;  // 0.01, q32
+    default:  return 0;
+  }
+  return d1 + d2 * get_raw();
+}
+
+
+int32_t Temp::get_fixed(int fbits) const {
+  return shift_round(get_q32(), 32-fbits);
+}
+
+
+// ---- Humidity ----
+
+// -- Floating point. --
+
+float Hum::get_f() const {
+  float c1 = -2.0468f;
+  float c2;
+  float c3;
+  float t1 = 0.01f;
+  float t2;
+  float r;
+  float raw = static_cast<float>(get_raw());
+  switch (get_bits()) {
+    case 8:
+      c2 = 0.5872f;
+      c3 = -4.0845E-4f;
+      t2 = 0.00128f;
+      break;
+    case 12:
+      c2 = 0.0367f;
+      c3 = -1.5955E-6f;
+      t2 = 0.00008f;
+      break;
+    default:
+      return std::numeric_limits<float>::quiet_NaN();
+  }
+  r = c1 + (c2 * raw) + (c3 * (raw * raw));
+  if (temp) {
+    r += (temp->get_f() - 25.0f)*(t1+t2*raw);
+  }
+  // Clamp as specified in the datasheet.
+  if (r < 0.0f) {
+    return 0.0f;
+  }
+  if (r > 99.0f) {
+    return 100.0f;
+  }
+  return r;
+}
+
+
+// -- Fixed point. --
+
+int64_t Hum::get_q32() const {
+  int64_t c1 = -0x20bfb15b5LL;    // -2.0468
+  int64_t c2;
+  int64_t c3;
+  int64_t t1 = 0x28f6LL;          // 0.01, q20
+  int64_t t2;
+  int64_t r;
+  int64_t raw = static_cast<int64_t>(get_raw());
+  switch (get_bits()) {
+    case 8:
+      c2 = 0x9652bd3cLL;  // 0.5872f;
+      c3 = -0x1ac4a7LL;   // -4.0845E-4f;
+      t2 = 0x53eLL;       // 0.00128f, q20;
+      break;
+    case 12:
+      c2 = 0x9652bd3LL;   // 0.0367f;
+      c3 = -0x1ac4LL;     // -1.5955E-6f;
+      t2 = 0x54LL;        // 0.00008f, q20;
+      break;
+    default:
+      return INT64_MIN;
+  }
+  // The 'c' coefficients are all in q32 format. The raw value is in q0
+  // (integer) format, so r will be in q32 format after this operation.
+  r = c1 + (c2 * raw) + (c3 * (raw * raw));
+  if (temp) {
+    int64_t co = t1 + (t2 * raw);   // q20
+    int64_t t = temp->get_q32() - (25LL << 32);
+    r += shift_round(co * t, 20);
+  }
+  // Clamp as specified in the datasheet.
+  if (r < 0LL) {
+    return 0LL;
+  }
+  if (r > (99LL<<32)) {
+    return 100LL<<32;
+  }
+  return r;
+}
+
+
+int32_t Hum::get_fixed(int fbits) const {
+  return shift_round(get_q32(), 32-fbits);
+}
+
+
+} // namespace sht7x