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.
Diff: sht7x.cpp
- 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