tmp102: Initial add of tmp102 driver. (#76)

pull/77/head
gsexton 2 years ago committed by GitHub
parent 2cf8fa10c9
commit f9d46888f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,21 @@
// 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.
//
// tmp102 provides a package for interfacing a Texas Instruments TMP102 I2C
// temperature sensor. This driver is also compatible with the TMP112 and
// TMP75 sensors.
//
// Range: -40°C - 125°C
//
// Accuracy: +/- 0.5°C
//
// Resolution: 0.0625°C
//
// For detailed information, refer to the [datasheet].
//
// A [command line example] is available in periph.io/x/devices/cmd/tmp102
//
// [datasheet]: https://www.ti.com/lit/ds/symlink/tmp102.pdf
// [command line example]: https://github.com/periph/cmd/tree/main/tmp102/
package tmp102

@ -0,0 +1,389 @@
// 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{}

@ -0,0 +1,176 @@
// 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 (
"testing"
"time"
"periph.io/x/conn/v3/i2c/i2ctest"
"periph.io/x/conn/v3/physic"
)
const (
// Default value for alert low range value.
DefaultLow physic.Temperature = physic.ZeroCelsius + 75*physic.Kelvin
// Default value for alert high range value.
DefaultHigh physic.Temperature = physic.ZeroCelsius + 80*physic.Kelvin
addr uint16 = 0x48
)
func defaultOps() []i2ctest.IO {
ops := []i2ctest.IO{
{Addr: addr, W: []byte{_REGISTER_CONFIGURATION}},
{Addr: addr, W: []byte{_REGISTER_CONFIGURATION, 0x00, 0x80}}, // Write the config
{Addr: addr, W: []byte{_REGISTER_RANGE_LOW, 0x4b, 0x00}}, // Write the low alert temp
{Addr: addr, W: []byte{_REGISTER_RANGE_HIGH, 0x50, 0x00}}, // Write the high alert temp
}
return ops
}
// TestSenseContinous test the sense continuous function, which
// implicitly tests Sense() and countToTemperature().
func TestSenseContinuous(t *testing.T) {
// A set of counts, and the expected temperature value.
tests := []struct {
bits []byte
expected physic.Temperature
}{
{[]byte{0x64, 0x00}, physic.ZeroCelsius + 100*physic.Kelvin},
{[]byte{0x50, 0x00}, physic.ZeroCelsius + 80*physic.Kelvin},
{[]byte{0x32, 0x00}, physic.ZeroCelsius + 50*physic.Kelvin},
{[]byte{0x19, 0x00}, physic.ZeroCelsius + 25*physic.Kelvin},
{[]byte{0x00, 0x00}, physic.ZeroCelsius},
{[]byte{0xe7, 0x00}, physic.ZeroCelsius - 25*physic.Kelvin},
{[]byte{0xc9, 0x00}, physic.ZeroCelsius - 55*physic.Kelvin},
}
opts := Opts{
SampleRate: RateFourHertz,
AlertSetting: ModeComparator,
AlertLow: DefaultLow,
AlertHigh: DefaultHigh,
}
ops := defaultOps()
// Add the test values to our playback bus.
for _, test := range tests {
ops = append(ops, i2ctest.IO{Addr: addr, W: []byte{_REGISTER_TEMPERATURE}, R: test.bits})
}
pb := &i2ctest.Playback{Ops: ops, DontPanic: true, Count: 1}
defer pb.Close()
record := &i2ctest.Record{Bus: pb}
tmp102, err := NewI2C(record, addr, &opts)
if err != nil {
t.Error(err)
return
}
ch, err := tmp102.SenseContinuous(250 * time.Millisecond)
if err != nil {
t.Error(err)
return
}
for count := 0; count < len(tests); count++ {
env := <-ch
t.Logf("Temperature = %.4f", env.Temperature.Celsius())
if env.Temperature != tests[count].expected {
t.Errorf("Error testing. Read: %.4f Expected %.4f", env.Temperature.Celsius(), tests[count].expected.Celsius())
}
}
err = tmp102.Halt()
if err != nil {
t.Error(err)
}
t.Logf("record.ops=%#v", record.Ops)
}
func TestString(t *testing.T) {
ops := defaultOps()
pb := &i2ctest.Playback{Ops: ops, DontPanic: true, Count: 1}
defer pb.Close()
record := &i2ctest.Record{Bus: pb}
tmp102, err := NewI2C(record, addr, nil)
if err != nil {
t.Error(err)
return
}
s := tmp102.String()
t.Log(s)
if len(s) == 0 {
t.Error("invalid String() result")
}
}
func TestSetAlertMode(t *testing.T) {
ops := make([]i2ctest.IO, 0)
ops = append(ops, []i2ctest.IO{
{Addr: addr, W: []byte{_REGISTER_CONFIGURATION}, R: []byte{0x00, 0x00}}, // Read the device config.
{Addr: addr, W: []byte{_REGISTER_CONFIGURATION, 0x00, 0x80}}, // Set the device config.
{Addr: addr, W: []byte{_REGISTER_RANGE_LOW}, R: []byte{0x4b, 0}}, // Read the low limit register
{Addr: addr, W: []byte{_REGISTER_RANGE_HIGH}, R: []byte{0x50, 0}}, // Read the High Limit Register
{Addr: addr, W: []byte{_REGISTER_RANGE_LOW, 0x4b, 0x80}}, // Set the read of the low limit to 75C
{Addr: addr, W: []byte{_REGISTER_RANGE_HIGH, 0x4f, 0x80}}, // Set the read of the high limit to 80C
{Addr: addr, W: []byte{_REGISTER_CONFIGURATION, 0x02, 0x00}}, // Add 1/2 Degree C to the range low
{Addr: addr, W: []byte{_REGISTER_CONFIGURATION}, R: []byte{0x02, 0}}, // Read the confugration register.
{Addr: addr, W: []byte{_REGISTER_RANGE_LOW}, R: []byte{0x4b, 0x80}}, // Read the low temp register
{Addr: addr, W: []byte{_REGISTER_RANGE_HIGH}, R: []byte{0x4f, 0x80}}, // Read the high temp register
{Addr: addr, W: []byte{_REGISTER_RANGE_LOW, 0x4b, 0x00}}, // write it back to 75C
{Addr: addr, W: []byte{_REGISTER_RANGE_HIGH, 0x50, 0x00}}, // set it back to 80C
}...)
pb := &i2ctest.Playback{Ops: ops, DontPanic: true, Count: 1}
defer pb.Close()
record := &i2ctest.Record{Bus: pb}
defer t.Logf("record=%#v", record)
tmp102, err := NewI2C(record, addr, nil)
if err != nil {
t.Error(err)
return
}
mode, low, high, err := tmp102.GetAlertMode()
t.Logf("newMode=%d, newLow=%.4f, newHigh=%.4f", mode, low.Celsius(), high.Celsius())
if err != nil {
t.Error(err)
}
var newMode AlertMode
if mode == ModeComparator {
newMode = ModeInterrupt
} else {
newMode = ModeComparator
}
newLow := low + 500*physic.MilliKelvin
newHigh := high - 500*physic.MilliKelvin
t.Logf("newMode=%d, newLow=%.4f, newHigh=%.4f", newMode, newLow.Celsius(), newHigh.Celsius())
err = tmp102.SetAlertMode(newMode, newLow, newHigh)
if err != nil {
t.Error(err)
}
checkMode, checkLow, checkHigh, err := tmp102.GetAlertMode()
t.Logf("checkMode=%d checkLow=%.4f checkHigh=%.4f", checkMode, checkLow.Celsius(), checkHigh.Celsius())
if err != nil {
t.Error(err)
}
if checkMode != newMode || checkLow != newLow || checkHigh != newHigh {
t.Errorf("Error setting/reading alert mode. Received: Mode=%d, Low=%.4f, High=%.4f. Expected: Mode:%d, Low=%.4f, High=%.4f",
checkMode, checkLow.Celsius(), checkHigh.Celsius(),
newMode, newLow.Celsius(), newHigh.Celsius())
}
err = tmp102.SetAlertMode(mode, low, high)
if err != nil {
t.Error(err)
}
checkMode, _, _, _ = tmp102.GetAlertMode()
if checkMode != mode {
t.Errorf("Error resetting mode. Got %d Expected %d", checkMode, mode)
}
}
Loading…
Cancel
Save