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.

sht7x.cpp

Committer:
JacobBramley
Date:
2013-08-11
Revision:
0:f1a93e55feb5

File content as of revision 0:f1a93e55feb5:

// 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