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/tmp102/tmp102.go

390 lines
10 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.
package tmp102
import (
"errors"
"fmt"
"sync"
"time"
"periph.io/x/conn/v3"
"periph.io/x/conn/v3/i2c"
"periph.io/x/conn/v3/physic"
)
type ConversionRate byte
type AlertMode byte
// Dev represents a TMP102 sensor.
type Dev struct {
d *i2c.Dev
shutdown chan bool
mu sync.Mutex
opts *Opts
}
const (
// Conversion (sample) Rates. The device default is 4 readings/second.
RateQuarterHertz ConversionRate = iota
RateOneHertz
RateFourHertz
RateEightHertz
// ModeComparator sets the device to operate in Comparator mode.
// Refer to section 6.4.5.1 of the TMP102 datasheet. When used, the
// ALERT pin of the TMP102 will trigger.
ModeComparator AlertMode = 0
// ModeInterrupt sets the device to operate in Interrupt mode.
// Note that reading the temperature will clear the alert, so be aware
// if you're using SenseContinuous.
ModeInterrupt AlertMode = 1
// Addresses of registers to read/write.
_REGISTER_TEMPERATURE byte = 0
_REGISTER_CONFIGURATION byte = 1
_REGISTER_RANGE_LOW byte = 2
_REGISTER_RANGE_HIGH byte = 3
// Bit numbers for various configuration operations.
_SHUTDOWN_BIT int = 8
_THERMOSTAT_MODE int = 9
_CONVERSION_RATE_POS int = 6
_DEGREES_RESOLUTION physic.Temperature = 62_500 * physic.MicroKelvin
// The minimum temperature in StandardMode the device can read.
MinimumTemperature physic.Temperature = physic.ZeroCelsius - 40*physic.Kelvin
// The maximum temperature in StandardMode the device can read.
MaximumTemperature physic.Temperature = physic.ZeroCelsius + 125*physic.Kelvin
)
// Opts represents configurable options for the TMP102.
type Opts struct {
SampleRate ConversionRate
AlertSetting AlertMode
AlertLow physic.Temperature
AlertHigh physic.Temperature
}
func (dev *Dev) isShutdown() bool {
return dev.shutdown == nil
}
// start initializes the device to a known state and ensures its
// not in shutdown mode.
func (dev *Dev) start() error {
config := dev.ReadConfiguration()
mask := uint16(0xffff) ^ (uint16(1<<_SHUTDOWN_BIT) | uint16(1<<_THERMOSTAT_MODE))
config &= mask
config |= uint16(dev.opts.AlertSetting) << _THERMOSTAT_MODE
cr := ConversionRate((config >> _CONVERSION_RATE_POS) & 0x03)
if cr != dev.opts.SampleRate {
// Turn off the sample rate bits.
config &= 0xffff ^ uint16(0x03<<_CONVERSION_RATE_POS)
// Now set the new value.
config |= uint16(dev.opts.SampleRate) << _CONVERSION_RATE_POS
}
var bits []byte
w := make([]byte, 3)
w[0] = _REGISTER_CONFIGURATION
w[1] = byte(config>>8) & 0xff
w[2] = byte(config & 0xff)
err := dev.d.Tx(w, nil)
if err != nil {
return err
}
dev.shutdown = make(chan bool)
if dev.opts.AlertLow != 0 {
bits, err = temperatureToCount(dev.opts.AlertLow)
if err != nil {
return err
}
w[0] = _REGISTER_RANGE_LOW
w[1] = bits[0]
w[2] = bits[1]
err = dev.d.Tx(w, nil)
if err != nil {
return err
}
}
if dev.opts.AlertHigh != 0 {
bits, err = temperatureToCount(dev.opts.AlertHigh)
if err != nil {
return err
}
w[0] = _REGISTER_RANGE_HIGH
w[1] = bits[0]
w[2] = bits[1]
err = dev.d.Tx(w, nil)
}
return err
}
// temperatureToCount converts a temperature into the count that the device
// uses. Required to set the Low/High Range registers for alerts.
func temperatureToCount(temp physic.Temperature) ([]byte, error) {
result := make([]byte, 2)
if temp == physic.ZeroCelsius {
return result, nil
}
negative := temp < physic.ZeroCelsius
var count uint16
if negative {
temp = physic.ZeroCelsius + physic.Temperature(-1*temp.Celsius())*physic.Kelvin
count = uint16((temp - physic.ZeroCelsius) / _DEGREES_RESOLUTION)
count = ((twosComplement(count) | (1 << 11)) + 1)
} else {
count = uint16((temp - physic.ZeroCelsius) / _DEGREES_RESOLUTION)
}
count = count << 4
result[0] = byte(count >> 8 & 0xff)
result[1] = byte(count & 0xf0)
return result, nil
}
func twosComplement(value uint16) uint16 {
var result uint16
for iter := 0; iter < 11; iter++ {
bitVal := uint16(1 << iter)
if (value & bitVal) == 0 {
result |= bitVal
}
}
return result
}
// countToTemperature returns the temperature from the raw device count.
func countToTemperature(bytes []byte) physic.Temperature {
var t physic.Temperature
count := (uint16(bytes[0]) << 4) | (uint16(bytes[1]) >> 4)
negative := (count & (1 << 11)) > 0
if negative {
count = twosComplement(count) + 1
t = physic.ZeroCelsius - (physic.Temperature(count) * _DEGREES_RESOLUTION)
} else {
t = physic.ZeroCelsius + (physic.Temperature(count) * _DEGREES_RESOLUTION)
}
return t
}
// readConfiguration returns the device's configuration registers as a 16 bit
// unsigned integer. Refer to the datasheet for interpretation.
func (dev *Dev) ReadConfiguration() uint16 {
w := make([]byte, 1)
w[0] = _REGISTER_CONFIGURATION
r := make([]byte, 2)
_ = dev.d.Tx(w, r)
result := uint16(r[0])<<8 | uint16(r[1])
return result
}
// readTemperature returns the raw counts from the device temperature registers.
func (dev *Dev) readTemperature() (physic.Temperature, error) {
var err error
if dev.isShutdown() {
err = dev.start()
if err != nil {
return MinimumTemperature, err
}
}
r := make([]byte, 2)
err = dev.d.Tx([]byte{_REGISTER_TEMPERATURE}, r)
if err != nil {
return MinimumTemperature, err
}
return countToTemperature(r), nil
}
// NewI2C returns a new TMP102 sensor using the specified bus and address.
// If opts is not supplied, the configuration of the sensor is set to the
// default on startup.
func NewI2C(b i2c.Bus, addr uint16, opts *Opts) (*Dev, error) {
if opts == nil {
opts = &Opts{SampleRate: RateFourHertz, AlertSetting: ModeComparator}
}
d := &Dev{d: &i2c.Dev{Bus: b, Addr: addr}, opts: opts, shutdown: nil}
return d, d.start()
}
// GetAlertMode returns the current alert settings for the device.
func (dev *Dev) GetAlertMode() (mode AlertMode, rangeLow, rangeHigh physic.Temperature, err error) {
dev.mu.Lock()
defer dev.mu.Unlock()
mode = AlertMode((dev.ReadConfiguration() >> _THERMOSTAT_MODE) & 0x01)
rangeLow = MinimumTemperature
rangeHigh = MaximumTemperature
w := make([]byte, 1)
r := make([]byte, 2)
w[0] = _REGISTER_RANGE_LOW
err = dev.d.Tx(w, r)
if err != nil {
return
}
rangeLow = countToTemperature(r)
w[0] = _REGISTER_RANGE_HIGH
err = dev.d.Tx(w, r)
if err != nil {
return
}
rangeHigh = countToTemperature(r)
return
}
// 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()
var err error
if dev.shutdown != nil {
close(dev.shutdown)
dev.shutdown = nil
}
current := dev.ReadConfiguration()
mask := uint16(0xffff ^ (1 << _SHUTDOWN_BIT))
new := current & mask
if current != new {
w := make([]byte, 3)
w[0] = _REGISTER_CONFIGURATION
w[1] = byte(new >> 8)
w[2] = byte(new & 0xff)
err = dev.d.Tx(w, nil)
}
return err
}
// Sense reads temperature from the device and writes the value to the specified
// env variable. Implements physic.SenseEnv.
func (dev *Dev) Sense(env *physic.Env) error {
dev.mu.Lock()
defer dev.mu.Unlock()
t, err := dev.readTemperature()
if err == nil {
env.Temperature = t
}
return err
}
// SenseContinuous continuously reads from the device and writes the value to
// the returned channel. Implements physic.SenseEnv. To terminate the
// continuous read, call Halt().
func (dev *Dev) SenseContinuous(interval time.Duration) (<-chan physic.Env, error) {
channelSize := 16
if interval < (125 * time.Millisecond) {
return nil, errors.New("invalid duration. minimum 125ms")
}
channel := make(chan physic.Env, channelSize)
go func(channel chan physic.Env, shutdown <-chan bool) {
ticker := time.NewTicker(interval)
for {
select {
case <-shutdown:
close(channel)
return
case <-ticker.C:
// do the reading and write to the channel.
e := physic.Env{}
err := dev.Sense(&e)
if err == nil && len(channel) < channelSize {
channel <- e
}
}
}
}(channel, dev.shutdown)
return channel, nil
}
// SetAlertMode sets the device to operate in alert (thermostat) mode. Alert
// mode will set the Alert pin on the device to active mode when the conditions
// apply. Refer to section 6.4.5 and section 6.5.4 of the TMP102 datasheet.
//
// To detect the alert trigger, you will need to connect the device ALERT pin
// to a GPIO pin on your SBC and configure that GPIO pin with edge detection,
// or continuously poll the GPIO pin state. If you choose polling, care should
// be taken if you're also using SenseContinuous.
func (dev *Dev) SetAlertMode(mode AlertMode, rangeLow, rangeHigh physic.Temperature) error {
if rangeLow >= rangeHigh ||
rangeLow < MinimumTemperature ||
rangeHigh > MaximumTemperature {
return errors.New("invalid temperature range")
}
dev.opts.AlertSetting = mode
dev.opts.AlertLow = rangeLow
dev.opts.AlertHigh = rangeHigh
var err error
dev.mu.Lock()
defer dev.mu.Unlock()
// Write the low range temperature
rangeBytes, _ := temperatureToCount(rangeLow)
w := make([]byte, 3)
w[0] = _REGISTER_RANGE_LOW
w[1] = rangeBytes[0]
w[2] = rangeBytes[1]
err = dev.d.Tx(w, nil)
if err != nil {
return err
}
// Write the High Range Temperature
rangeBytes, _ = temperatureToCount(rangeHigh)
w[0] = _REGISTER_RANGE_HIGH
w[1] = rangeBytes[0]
w[2] = rangeBytes[1]
err = dev.d.Tx(w, nil)
if err != nil {
return err
}
// Check if the device is in shutdown, or if the mode has
// changed, and update the device running configuration
running := dev.ReadConfiguration()
mask := uint16(0xffff ^ ((1 << _SHUTDOWN_BIT) | (1 << _THERMOSTAT_MODE)))
new := (running & mask) | uint16(mode)<<_THERMOSTAT_MODE
if new != running {
w[0] = _REGISTER_CONFIGURATION
w[1] = byte(new >> 8)
w[2] = byte(new & 0xff)
err = dev.d.Tx(w, nil)
}
return err
}
// Precision returns the sensor's precision, or minimum value between steps the
// device can make. The specified precision is 0.0625 degrees Celsius. Note
// that the accuracy of the device is +/- 0.5 degrees Celsius.
func (dev *Dev) Precision(env *physic.Env) {
env.Temperature = _DEGREES_RESOLUTION
env.Pressure = 0
env.Humidity = 0
}
func (dev *Dev) String() string {
return fmt.Sprintf("tmp102: %s", dev.d.String())
}
var _ conn.Resource = &Dev{}
var _ physic.SenseEnv = &Dev{}