mirror of https://github.com/periph/devices
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.
390 lines
10 KiB
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{}
|