diff --git a/tmp102/doc.go b/tmp102/doc.go new file mode 100644 index 0000000..fefe633 --- /dev/null +++ b/tmp102/doc.go @@ -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 diff --git a/tmp102/tmp102.go b/tmp102/tmp102.go new file mode 100644 index 0000000..f147c22 --- /dev/null +++ b/tmp102/tmp102.go @@ -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{} diff --git a/tmp102/tmp102_test.go b/tmp102/tmp102_test.go new file mode 100644 index 0000000..be0a8b2 --- /dev/null +++ b/tmp102/tmp102_test.go @@ -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) + } +}