You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
devices/hdc302x/hdc302x.go

680 lines
19 KiB
Go

// Copyright 2024 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
// This package provides a driver for the Texas Instruments HDC3021/3022
// I2C Temperature/Humidity Sensors. This is a high accuracy sensor with
// very good resolution.
//
// Datasheet
//
// https://www.ti.com/lit/ds/symlink/hdc3022.pdf
package hdc302x
import (
"errors"
"fmt"
"math"
"sync"
"time"
"periph.io/x/conn/v3"
"periph.io/x/conn/v3/i2c"
"periph.io/x/conn/v3/physic"
)
type SampleRate uint16
const (
// Constants for the sample rate to use for the measurements. The datasheet
// recommends not sampling more often than once per second to avoid self-heating
// of the sensor.
//
// Every other second
RateHalfHertz SampleRate = iota
// Sample 1x Second.
RateHertz
RateTwoHertz
RateFourHertz
Rate10Hertz
)
// Dev represents a hdc302x sensor.
type Dev struct {
d *i2c.Dev
shutdown chan struct{}
mu sync.Mutex
sampleRate SampleRate
halted bool
}
// The alert function works with pairs of values Temperature/Humidity. A
// Threshold is a set of humidity/temperature values defining an upper or
// lower limit for alerts.
type Threshold struct {
Humidity physic.RelativeHumidity
Temperature physic.Temperature
}
// For alert and clear, there is a pair of temperatures. For example,
// a low alert value, and a clear low alert value. There's a value
// for each measurement parameter.
type ThresholdPair struct {
Low Threshold
High Threshold
}
type StatusWord uint16
const (
// Status flags returned by ReadStatus()
StatusActiveAlerts StatusWord = 1 << 15
StatusHeaterEnabled StatusWord = 1 << 13
// Mirrored on the alert pin.
StatusRHTrackingAlert StatusWord = 1 << 11
// Also reflected on alert pin
StatusTempTrackingAlert StatusWord = 1 << 10
StatusRHHighTrackingAlert StatusWord = 1 << 9
StatusRHLowTrackingAlert StatusWord = 1 << 8
StatusTempHighTrackingAlert StatusWord = 1 << 7
StatusTempLowTrackingAlert StatusWord = 1 << 6
StatusDeviceReset StatusWord = 1 << 4
// Set if there was a CRC error on the last write command.
StatusLastWriteCRCFailure StatusWord = 1 << 0
)
// Configuration provides information about the running device's config.
type Configuration struct {
// Device unique ID. Read-Only
SerialNumber int64
// Numeric vendor ID. Read-Only
VendorID uint16
// Status Word. Refer to the Status* constants above, and the datasheet for
// usage.
Status StatusWord
// refer to the Rate constants. Read-Only
SampleRate SampleRate
// Offset for RH calculation. Note that these offsets are approximate,
// so a request to set the offset to -5%rH may result in an offset of
// -4.8%rH. This is an artifact of the device's offset implementation.
// Refer to the datasheet for more information.
HumidityOffset physic.RelativeHumidity
// Offset for Temp result. Note that the data sheet states this is not
// used in the RH calculation.
TemperatureOffset physic.Temperature
// High/Low thresholds for triggering alerts. As with the offsets,
// written values are not precise.
AlertThresholds ThresholdPair
// High/Low threshold for clearing alerts.
ClearThresholds ThresholdPair
}
const (
// The default i2c bus address for this device.
DefaultSensorAddress uint16 = 0x44
)
type HeaterPower uint16
const (
// Constants for setting the heater's power setting.
PowerFull HeaterPower = 0x3fff
PowerHalf HeaterPower = 0x03ff
PowerQuarter HeaterPower = 0x9f
PowerOff HeaterPower = 0
)
type devCommand []byte
// Sample Rate commands
var measure2Seconds = devCommand{0x20, 0x32}
var measureSecond = devCommand{0x21, 0x30}
var measure2xSecond = devCommand{0x22, 0x36}
var measure4xSecond = devCommand{0x23, 0x34}
var measure10xSecond = devCommand{0x27, 0x37}
var sampleRateCommands = []devCommand{measure2Seconds, measureSecond, measure2xSecond, measure4xSecond, measure10xSecond}
var sampleRateDurations = []time.Duration{2 * time.Second, time.Second, 500 * time.Millisecond, 250 * time.Millisecond, 100 * time.Millisecond}
// Other device commands
var clearStatus = devCommand{0x30, 0x41}
var disableHeater = devCommand{0x30, 0x66}
var enableHeater = devCommand{0x30, 0x6d}
var read = devCommand{0xe0, 0x0}
var readSetHeater = devCommand{0x30, 0x6e}
var readSetOffsets = devCommand{0xa0, 0x04}
var readStatus = devCommand{0xf3, 0x2d}
var readVendorID = devCommand{0x37, 0x81}
var reset = devCommand{0x30, 0xa2}
var stopContinuousReadings = devCommand{0x30, 0x93}
// read/write alert threshold commands.
var readLowAlertThresholds = devCommand{0xe1, 0x02}
var readHighAlertThresholds = devCommand{0xe1, 0x1f}
var readLowClearThresholds = devCommand{0xe1, 0x09}
var readHighClearThresholds = devCommand{0xe1, 0x14}
var writeLowAlertThresholds = devCommand{0x61, 0x00}
var writeHighAlertThresholds = devCommand{0x61, 0x1d}
var writeLowClearThresholds = devCommand{0x61, 0x0b}
var writeHighClearThresholds = devCommand{0x61, 0x16}
var errInvalidCRC = errors.New("hdc302x: invalid crc")
const (
// Magic numbers for count to value conversions.
temperatureOffset float64 = -45.0
temperatureScalar float64 = 175.0
humidityScalar float64 = 100.0
scaleDivisor float64 = 65535.0
)
// NewI2C returns a new HDC302x sensor using the specified bus, address, and
// sample rate.
func NewI2C(b i2c.Bus, addr uint16, sampleRate SampleRate) (*Dev, error) {
dev := &Dev{d: &i2c.Dev{Bus: b, Addr: addr}, shutdown: nil, sampleRate: sampleRate}
return dev, dev.start()
}
// send continuous measurement start command.
func (dev *Dev) start() error {
if err := dev.d.Tx(sampleRateCommands[dev.sampleRate], nil); err != nil {
return fmt.Errorf("hdc302x: init %w", err)
}
// Sleep for a minimum of one sample acquisition period. If you
// read before a sample has acquired, you get remote I/O error.
time.Sleep(sampleRateDurations[dev.sampleRate])
dev.halted = false
return nil
}
// Convert the raw count to a temperature.
func countToTemperature(bytes []byte) physic.Temperature {
count := (uint16(bytes[0]) << 8) | uint16(bytes[1])
f := float64(count)/float64(scaleDivisor)*temperatureScalar + temperatureOffset
t := physic.ZeroCelsius + physic.Temperature(f*float64(physic.Celsius))
return t
}
// convert the raw count to a humidity value.
func countToHumidity(bytes []byte) physic.RelativeHumidity {
count := (uint16(bytes[0]) << 8) | uint16(bytes[1])
f := float64(count) / float64(scaleDivisor) * humidityScalar
return physic.RelativeHumidity(f * float64(physic.PercentRH))
}
func crc8(bytes []byte) byte {
var crc byte = 0xff
for _, val := range bytes {
crc ^= val
for range 8 {
if (crc & 0x80) == 0 {
crc <<= 1
} else {
crc = (byte)((crc << 1) ^ 0x31)
}
}
}
return crc
}
// Halt shuts down the device. If a SenseContinuous operation is in progress,
// its aborted. Implements conn.Resource
func (dev *Dev) Halt() error {
dev.mu.Lock()
defer dev.mu.Unlock()
if dev.shutdown != nil {
close(dev.shutdown)
}
var err error
if !dev.halted {
dev.halted = true
err = dev.d.Tx(stopContinuousReadings, nil)
}
return err
}
// Sense reads temperature and humidity from the device and writes the value to
// the specified env variable. Implements physic.SenseEnv.
func (dev *Dev) Sense(env *physic.Env) error {
env.Temperature = 0
env.Pressure = 0
env.Humidity = 0
res := make([]byte, 6)
dev.mu.Lock()
defer dev.mu.Unlock()
if dev.halted {
if err := dev.start(); err != nil {
return err
}
}
if err := dev.d.Tx(read, res); err != nil {
return fmt.Errorf("hdc302x: %w", err)
}
if crc8(res[:2]) != res[2] || crc8(res[3:5]) != res[5] {
return errInvalidCRC
}
env.Temperature = countToTemperature(res)
env.Humidity = countToHumidity(res[3:])
return nil
}
func temperatureToFloat64(temp physic.Temperature) float64 {
return float64(temp) / float64(physic.Celsius)
}
func humidityToFloat64(humidity physic.RelativeHumidity) float64 {
return float64(humidity) / float64(physic.PercentRH)
}
// SenseContinuous continuously reads from the device and writes the value to
// the returned channel. Implements physic.SenseEnv. To terminate the
// continuous read, call Halt().
//
// If interval is less than the device sample period, an error is returned.
func (dev *Dev) SenseContinuous(interval time.Duration) (<-chan physic.Env, error) {
if dev.shutdown != nil {
return nil, errors.New("hdc302x: SenseContinuous already running")
}
if interval < sampleRateDurations[dev.sampleRate] {
return nil, errors.New("hdc302x: sample interval is < device sample rate")
}
dev.shutdown = make(chan struct{})
chResult := make(chan physic.Env, 16)
go func(ch chan physic.Env) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
defer close(ch)
for {
select {
case <-dev.shutdown:
dev.shutdown = nil
return
case <-ticker.C:
env := physic.Env{}
if err := dev.Sense(&env); err == nil {
ch <- env
}
}
}
}(chResult)
return chResult, nil
}
// Precision returns the sensor's precision, or minimum value between steps the
// device can measure. Refer to the datasheet for information on limits and
// accuracy.
func (dev *Dev) Precision(env *physic.Env) {
env.Temperature = physic.Temperature(math.Round(temperatureScalar / scaleDivisor * float64(physic.Celsius)))
env.Humidity = physic.RelativeHumidity(math.Round((float64(physic.PercentRH) * humidityScalar) / float64(scaleDivisor)))
env.Pressure = 0
}
func (dev *Dev) readSerialNumber() int64 {
var result int64
cmd := []byte{0x36, 0x83}
r := make([]byte, 3)
// this is a 6 byte value read in 3 parts
for range 3 {
err := dev.d.Tx(cmd, r)
if err != nil || (crc8(r[:2]) != r[2]) {
return result
}
result = result<<16 | (int64(r[0])<<8 | int64(r[1]))
cmd[1] += 1 // Increment the register to the next one.
}
return result
}
// read the alert / clear threshold values from the device.
func (dev *Dev) readAlertValues(cfg *Configuration) error {
// Pair
// Low
// Temp
// Humidity
// High
// Temp
// Humidity
var cmds = []devCommand{readLowAlertThresholds, readHighAlertThresholds, readLowClearThresholds, readHighClearThresholds}
var pairs = [2]*ThresholdPair{&cfg.AlertThresholds, &cfg.ClearThresholds}
var threshold *Threshold
r := make([]byte, 3)
for ix, cmd := range cmds {
pair := pairs[ix>>1]
if ix%2 == 0 {
threshold = &pair.Low
} else {
threshold = &pair.High
}
err := dev.d.Tx(cmd, r)
if err != nil {
return err
}
if crc8(r[:2]) != r[2] {
return errInvalidCRC
}
wValue := uint16(r[0])<<8 | uint16(r[1])
// The alert value is returned as a 16 bit words, where bits 0-8 are the
// Temperature value, and bits 9-15 are the Humidity. The temperature
// bits correspond to bits 7-15 of the temperature, and bits 9-15 of the
// humidity. Refer to the datasheet.
temp := &threshold.Temperature
humidity := &threshold.Humidity
*temp = physic.Temperature(((float64(uint16(wValue<<7)) * temperatureScalar) / scaleDivisor) * float64(physic.Celsius))
*humidity = physic.RelativeHumidity(((float64(wValue&0xfe00) * humidityScalar) / scaleDivisor) * float64(physic.PercentRH))
}
return nil
}
// readOffsets returns temperature/humidity offset values stored to the device.
func (dev *Dev) readOffsets(cfg *Configuration) error {
r := make([]byte, 3)
if err := dev.d.Tx(readSetOffsets, r); err != nil {
return fmt.Errorf("hdc302x: %w", err)
}
if crc8(r[:2]) != r[2] {
return errInvalidCRC
}
// The result comes back as the humidity offset, followed by
// the temperature offset. The offsets are computed by summing
// the bits and applying the partial algorithm.
h := uint16((r[0] & 0x7f)) << 7
t := uint16((r[1] & 0x7f)) << 6
rh := float64(h) / scaleDivisor * humidityScalar
temp := (float64(t) * temperatureScalar) / scaleDivisor
if r[0]&0x80 == 0x00 {
rh *= -1.0
}
cfg.HumidityOffset = physic.RelativeHumidity(rh * float64(physic.PercentRH))
if r[1]&0x80 == 0x00 {
temp *= -1.0
}
cfg.TemperatureOffset = physic.Temperature(temp * float64(physic.Celsius))
return nil
}
func (dev *Dev) readVendorID() (uint16, error) {
r := make([]byte, 3)
err := dev.d.Tx(readVendorID, r)
if err == nil {
vid := uint16(r[0])<<8 | uint16(r[1])
return vid, nil
}
return 0, err
}
// ReadStatus returns the device's status word, and if successful, clears the
// status. Refer to the Status* constants and the datasheet for interpretation.
func (dev *Dev) ReadStatus() (StatusWord, error) {
r := make([]byte, 3)
if err := dev.d.Tx(readStatus, r); err != nil {
return 0, err
}
if crc8(r[:2]) != r[2] {
return 0, errInvalidCRC
}
_ = dev.d.Tx(clearStatus, nil)
return StatusWord(r[0])<<8 | StatusWord(r[1]), nil
}
// Return the device's configuration settings. Includes alert values, offset
// values, and other information about the device.
func (dev *Dev) Configuration() (*Configuration, error) {
cfg := &Configuration{SampleRate: dev.sampleRate}
cfg.SerialNumber = dev.readSerialNumber()
err := dev.readOffsets(cfg)
if err != nil {
return cfg, err
}
if cfg.VendorID, err = dev.readVendorID(); err != nil {
return cfg, err
}
if cfg.Status, err = dev.ReadStatus(); err != nil {
return cfg, err
}
err = dev.readAlertValues(cfg)
return cfg, err
}
// setOffsets writes temperature and humidity offsets to the device.
// Refer to the datasheet for information on offsets. The critical
// thing to know is that the smallest offsets are ~0.2%RH, and ~
// 0.2 degrees C.
func (dev *Dev) setOffsets(cfg *Configuration) error {
var w = []byte{readSetOffsets[0],
readSetOffsets[1],
computeHumidityOffsetByte(cfg.HumidityOffset),
computeTemperatureOffsetByte(cfg.TemperatureOffset),
0,
}
w[4] = crc8(w[2:4])
return dev.d.Tx(w, nil)
}
// Refer to the datasheet. Essentially, the offsets are only a specific set of
// bit ranges.
func computeTemperatureOffsetByte(temp physic.Temperature) byte {
var res byte
fTemp := temperatureToFloat64(temp)
if fTemp >= 0 {
res |= 0x80
} else {
fTemp *= -1.0
}
for bit := 12; bit > 5; bit-- {
offset := (float64(int64(1)<<bit) * temperatureScalar) / scaleDivisor
if fTemp >= offset {
fTemp -= offset
res |= (1 << (bit - 6))
}
}
return res
}
func computeHumidityOffsetByte(humidity physic.RelativeHumidity) byte {
var res byte
fHumidity := humidityToFloat64(humidity)
if fHumidity >= 0 {
res |= 0x80
} else {
fHumidity *= -1.0
}
for bit := 13; bit > 6; bit-- {
offset := (float64(int64(1)<<bit) * humidityScalar) / scaleDivisor
if fHumidity >= offset {
fHumidity -= offset
res |= (1 << (bit - 7))
}
}
return res
}
// Reset performs a soft-reset of the device.
func (dev *Dev) Reset() error {
dev.mu.Lock()
defer dev.mu.Unlock()
err := dev.d.Tx(reset, nil)
time.Sleep(time.Second)
return err
}
// setThreshold sets a threshold pair for either alert, or clear alert.
// if typeAlert is true, it indicates the pair type is alert, otherwise
// it's clear alert.
func (dev *Dev) setThresholds(typeAlert bool, tp *ThresholdPair) error {
var cmds = [][]devCommand{{writeLowAlertThresholds, writeHighAlertThresholds},
{writeLowClearThresholds, writeHighClearThresholds}}
pair := 1
if typeAlert {
pair = 0
}
var th *Threshold
for ix := range 2 {
if ix == 0 {
th = &tp.Low
} else {
th = &tp.High
}
temp := temperatureToFloat64(th.Temperature)
tempBits := uint16(0)
for bit := 15; bit >= 0; bit-- {
bitVal := (float64(uint16(1<<bit)) * temperatureScalar) / scaleDivisor
if temp >= bitVal {
temp -= bitVal
tempBits |= (1 << bit)
}
}
humidity := humidityToFloat64(th.Humidity)
humBits := uint16(0)
for bit := 15; bit >= 0; bit-- {
bitVal := (float64(uint16(1<<bit)) * humidityScalar) / scaleDivisor
if humidity >= bitVal {
humidity -= bitVal
humBits |= (1 << bit)
}
}
wval := uint16(0)
wval = (humBits & 0xfe00) | tempBits>>7
w := []byte{cmds[pair][ix][0], cmds[pair][ix][1], byte(wval >> 8), byte(wval & 0xff), 0}
w[4] = crc8(w[2:4])
err := dev.d.Tx(w, nil)
if err != nil {
return err
}
}
return nil
}
// SetConfiguration takes a modified configuration struct and
// applies it to the device.
func (dev *Dev) SetConfiguration(cfg *Configuration) error {
_ = dev.Halt()
dev.mu.Lock()
defer dev.mu.Unlock()
current, err := dev.Configuration()
if err != nil {
return err
}
if current.HumidityOffset != cfg.HumidityOffset || current.TemperatureOffset != cfg.TemperatureOffset {
if err := dev.setOffsets(cfg); err != nil {
return err
}
}
if !current.AlertThresholds.Equals(&cfg.AlertThresholds) {
if err := dev.setThresholds(true, &cfg.AlertThresholds); err != nil {
return err
}
}
if !current.ClearThresholds.Equals(&cfg.ClearThresholds) {
if err := dev.setThresholds(false, &cfg.ClearThresholds); err != nil {
return err
}
}
return nil
}
// The hdc302x sensors have a built in heater element for operating in environments
// where the humidity/temperature level is condensing. SetHeater allows you to turn
// the heater element on and off at specified power levels. Refer to the datasheet
// for instructions on how the heater can be used in those environments.
func (dev *Dev) SetHeater(powerLevel HeaterPower) error {
if powerLevel > PowerFull {
return fmt.Errorf("hdc302x: invalid value for powerLevel: 0x%x", powerLevel)
}
if powerLevel == PowerOff {
return dev.d.Tx(disableHeater, nil)
}
var setValue = []byte{readSetHeater[0],
readSetHeater[1],
byte((powerLevel >> 8) & 0xff),
byte(powerLevel & 0xff),
0}
setValue[4] = crc8(setValue[2:4])
err := dev.d.Tx(setValue, nil)
if err != nil {
return err
}
return dev.d.Tx(enableHeater, nil)
}
func (dev *Dev) String() string {
return fmt.Sprintf("hdc302x: %s", dev.d.String())
}
func (cfg *Configuration) String() string {
return fmt.Sprintf(`{
SerialNumber: 0x%x,
VendorID: 0x%x,
Status: 0x%x,
SampleRate: %d,
HumidityOffset: %s,
TemperatureOffset: %s,
AlertThresholds: %s,
ClearThresholds: %s
}`,
cfg.SerialNumber,
cfg.VendorID,
cfg.Status,
cfg.SampleRate,
cfg.HumidityOffset,
cfg.TemperatureOffset+physic.ZeroCelsius,
&cfg.AlertThresholds,
&cfg.ClearThresholds)
}
func (t *Threshold) String() string {
return fmt.Sprintf("{ Humidity: %s, Temperature: %s }", t.Humidity, t.Temperature+physic.ZeroCelsius)
}
func (tp *ThresholdPair) String() string {
return fmt.Sprintf("{ Low: %s, High: %s }",
&tp.Low,
&tp.High)
}
func (t *Threshold) Equals(tCompare *Threshold) bool {
return t.Temperature == tCompare.Temperature && t.Humidity == tCompare.Humidity
}
// For thresholds, you can only set a truncated value. For temperature, that means
// the 9 high bits, and for humidity, the 7 high bits. This means a comparison of
// a written value with the resulting value can be off. This method encapsulates
// the comparison of a threshold pair to make sure they're approximately equal.
func (t *Threshold) ApproximatelyEquals(tCompare *Threshold) bool {
t1 := temperatureToFloat64(t.Temperature)
h1 := humidityToFloat64(t.Humidity)
t2 := temperatureToFloat64(tCompare.Temperature)
h2 := humidityToFloat64(tCompare.Humidity)
tLimit := float64(uint16(1<<8)) * temperatureScalar / scaleDivisor
hLimit := float64(uint16(1<<9)) * humidityScalar / scaleDivisor
return math.Abs(t1-t2) < tLimit &&
math.Abs(h1-h2) < hLimit
}
func (tp *ThresholdPair) Equals(tpCompare *ThresholdPair) bool {
return tp.Low.Equals(&tpCompare.Low) && tp.High.Equals(&tpCompare.High)
}
var _ conn.Resource = &Dev{}
var _ physic.SenseEnv = &Dev{}