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.
491 lines
14 KiB
Go
491 lines
14 KiB
Go
// Copyright 2020 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 tlv493d
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
"periph.io/x/conn/v3/i2c"
|
|
"periph.io/x/conn/v3/physic"
|
|
)
|
|
|
|
// I2CAddr is the default I2C address for the TLV493D component.
|
|
const I2CAddr uint16 = 0x5e
|
|
|
|
// I2CAddr1 is an alternative I2C address for TLV493D components.
|
|
const I2CAddr1 uint16 = 0x1f
|
|
|
|
// Precision represents a request for a compromise between I2C bandwidth
|
|
// versus measurement precision.
|
|
type Precision int
|
|
|
|
const (
|
|
// HighPrecisionWithTemperature reads the full 12-bit value for each axis
|
|
// and the temperature
|
|
HighPrecisionWithTemperature Precision = 0
|
|
// LowPrecision reads only 8-bits for each axis. Temperature is not read.
|
|
LowPrecision Precision = 1
|
|
// DefaultTemperatureOffsetCompensation is the temperature offset compensation used by default, following the documentation advices.
|
|
// For accurate results, please calibrate it using CalibrateTemperatureOffsetCompensation method.
|
|
DefaultTemperatureOffsetCompensation = 340
|
|
)
|
|
|
|
const (
|
|
numberOfReadRegisters = 10
|
|
numberOfMeasurementRegisters = 7
|
|
numberOfFastMeasurementRegisters = 3
|
|
startupDelay = 40 * time.Millisecond
|
|
commandRecovery = 0xff
|
|
|
|
registerBx = 0
|
|
registerBy = 1
|
|
registerBz = 2
|
|
registerTemp = 3
|
|
registerBx2 = 4
|
|
registerBz2 = 5
|
|
registerTemp2 = 6
|
|
registerFactSet1 = 7
|
|
registerFactSet2 = 8
|
|
registerFactSet3 = 9
|
|
|
|
bitParity = 7
|
|
bitFastMode = 1
|
|
bitLowPowerMode = 0
|
|
bitLowPowerPeriod = 6
|
|
bitInterruptPad = 2
|
|
bitTemperatureMeasurement = 7
|
|
bitParityTest = 5
|
|
|
|
magneticFluxScaling = 98 * physic.MicroTesla
|
|
temperatureScaling = 1100 * physic.MilliCelsius
|
|
temperatureScalingDiv = 10
|
|
referenceTemperature = 25*physic.Celsius + physic.ZeroCelsius
|
|
)
|
|
|
|
// Mode reprents the various power modes described in the documentation
|
|
type Mode struct {
|
|
fastMode bool
|
|
lowPowerMode bool
|
|
lowPowerPeriod bool
|
|
timeToMeasure time.Duration
|
|
measurementFrequency physic.Frequency
|
|
}
|
|
|
|
// PowerDownMode shuts down the sensor. It can still reply to I2C commands.
|
|
var PowerDownMode = Mode{
|
|
fastMode: false,
|
|
lowPowerMode: false,
|
|
lowPowerPeriod: false,
|
|
timeToMeasure: 10 * time.Millisecond,
|
|
measurementFrequency: 1 * physic.Hertz,
|
|
}
|
|
|
|
// FastMode is the mode using the most energy
|
|
var FastMode = Mode{
|
|
fastMode: true,
|
|
lowPowerMode: false,
|
|
lowPowerPeriod: false,
|
|
timeToMeasure: 0,
|
|
measurementFrequency: 3300 * physic.Hertz,
|
|
}
|
|
|
|
// LowPowerMode uses less energy than FastMode, with lower measurement rate
|
|
var LowPowerMode = Mode{
|
|
fastMode: false,
|
|
lowPowerMode: true,
|
|
lowPowerPeriod: true,
|
|
timeToMeasure: 0,
|
|
measurementFrequency: 100 * physic.Hertz,
|
|
}
|
|
|
|
// UltraLowPowerMode saves the most energy but with very low measurement rate
|
|
var UltraLowPowerMode = Mode{
|
|
fastMode: false,
|
|
lowPowerMode: true,
|
|
lowPowerPeriod: false,
|
|
timeToMeasure: 0,
|
|
measurementFrequency: 10 * physic.Hertz,
|
|
}
|
|
|
|
// MasterControlledMode refer to TLV493D documentation on how to use this mode
|
|
var MasterControlledMode = Mode{
|
|
fastMode: true,
|
|
lowPowerMode: true,
|
|
lowPowerPeriod: true,
|
|
timeToMeasure: 0,
|
|
measurementFrequency: 3300 * physic.Hertz,
|
|
}
|
|
|
|
// Opts holds the configuration options.
|
|
type Opts struct {
|
|
I2cAddress uint16
|
|
Reset bool
|
|
Mode Mode
|
|
InterruptPadEnabled bool // If enabled, this can cause I2C failures. See documentation.
|
|
EnableTemperatureMeasurement bool // Disable to save power.
|
|
ParityTestEnabled bool
|
|
TemperatureOffsetCompensation int
|
|
}
|
|
|
|
// DefaultOpts are the recommended default options.
|
|
var DefaultOpts = Opts{
|
|
I2cAddress: I2CAddr,
|
|
Reset: true,
|
|
Mode: PowerDownMode,
|
|
EnableTemperatureMeasurement: true,
|
|
InterruptPadEnabled: false,
|
|
ParityTestEnabled: true,
|
|
TemperatureOffsetCompensation: DefaultTemperatureOffsetCompensation, // As per the documentation, can be calibrated for better precision
|
|
}
|
|
|
|
// Sample contains the metrics measured by the sensor
|
|
type Sample struct {
|
|
Bx physic.MagneticFluxDensity
|
|
By physic.MagneticFluxDensity
|
|
Bz physic.MagneticFluxDensity
|
|
Temperature physic.Temperature
|
|
}
|
|
|
|
// Dev is an handle to a TLV493D hall effect sensor.
|
|
type Dev struct {
|
|
mu sync.Mutex
|
|
i2c i2c.Dev
|
|
stop chan struct{}
|
|
continuousReadWG sync.WaitGroup
|
|
|
|
registersBuffer []byte
|
|
|
|
mode Mode
|
|
enableTemperatureMeasurement bool
|
|
interruptPadEnabled bool
|
|
parityTestEnabled bool
|
|
|
|
temperatureOffsetCompensation int
|
|
}
|
|
|
|
// New creates a new TLV493D driver for a 3D hall effect sensors
|
|
func New(i i2c.Bus, opts *Opts) (*Dev, error) {
|
|
switch opts.I2cAddress {
|
|
case I2CAddr, I2CAddr1:
|
|
default:
|
|
return nil, errors.New("TLV493D: given address not supported by device")
|
|
}
|
|
|
|
d := &Dev{
|
|
i2c: i2c.Dev{Bus: i, Addr: opts.I2cAddress},
|
|
mode: opts.Mode,
|
|
enableTemperatureMeasurement: opts.EnableTemperatureMeasurement,
|
|
interruptPadEnabled: opts.InterruptPadEnabled,
|
|
parityTestEnabled: opts.ParityTestEnabled,
|
|
temperatureOffsetCompensation: opts.TemperatureOffsetCompensation,
|
|
registersBuffer: make([]byte, numberOfReadRegisters),
|
|
}
|
|
if err := d.initialize(opts); err != nil {
|
|
return nil, err
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
// String implements conn.Resource.
|
|
func (d *Dev) String() string {
|
|
return "TLV493D"
|
|
}
|
|
|
|
// Halt implements conn.Resource.
|
|
func (d *Dev) Halt() error {
|
|
// Stop any continuous read
|
|
d.StopContinousRead()
|
|
|
|
return d.SetMode(PowerDownMode)
|
|
}
|
|
|
|
func (d *Dev) initialize(opts *Opts) error {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
|
|
time.Sleep(startupDelay)
|
|
|
|
// Send recovery
|
|
if err := d.i2c.Tx([]byte{commandRecovery}, nil); err != nil {
|
|
return err
|
|
}
|
|
|
|
if opts.Reset {
|
|
// Reset I2C address
|
|
var resetAddress byte = 0x00
|
|
if d.i2c.Addr == I2CAddr1 {
|
|
resetAddress = 0xff
|
|
}
|
|
if err := d.i2c.Tx([]byte{resetAddress}, nil); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Read all 10 registers and store it as the initial data
|
|
if err := d.i2c.Tx([]byte{registerBx}, d.registersBuffer); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Configure sensor
|
|
return d.configure()
|
|
}
|
|
|
|
func (d *Dev) configure() error {
|
|
|
|
// Configure sensor
|
|
config1 := d.registersBuffer[registerFactSet1]
|
|
config2 := d.registersBuffer[registerFactSet3]
|
|
|
|
// Unset parity bit first
|
|
config1 = setBit(config1, bitParity, false)
|
|
|
|
// Mode
|
|
config1 = setBit(config1, bitFastMode, d.mode.fastMode)
|
|
config1 = setBit(config1, bitLowPowerMode, d.mode.lowPowerMode)
|
|
config2 = setBit(config2, bitLowPowerPeriod, d.mode.lowPowerPeriod)
|
|
|
|
// Temperature: set to 0 to enable it
|
|
config2 = setBit(config2, bitTemperatureMeasurement, !d.enableTemperatureMeasurement)
|
|
|
|
// Other configuration bits
|
|
config1 = setBit(config1, bitInterruptPad, d.interruptPadEnabled)
|
|
config2 = setBit(config2, bitParityTest, d.parityTestEnabled)
|
|
|
|
configBuffer := []byte{
|
|
0x00,
|
|
config1,
|
|
d.registersBuffer[registerFactSet2],
|
|
config2,
|
|
}
|
|
|
|
// Parity: the number of bits set must be odd
|
|
// As we unset the parity bit first, if the number of bits is currently even, we have to set it
|
|
// to make the number of bits set odd
|
|
configBuffer[1] = setBit(config1, 7, isNumberOfBitsEven(configBuffer))
|
|
|
|
if err := d.i2c.Tx(configBuffer, nil); err != nil {
|
|
return fmt.Errorf("unable to read configuration: %#v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetMode sets the power mode of the sensor
|
|
func (d *Dev) SetMode(mode Mode) error {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
|
|
d.mode = mode
|
|
return d.configure()
|
|
}
|
|
|
|
// EnableTemperatureMeasurement controls the temperature sensor activation
|
|
func (d *Dev) EnableTemperatureMeasurement(enable bool) error {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
|
|
d.enableTemperatureMeasurement = enable
|
|
return d.configure()
|
|
}
|
|
|
|
// EnableInterruptions controls if the sensor should send interruption of new measurement
|
|
func (d *Dev) EnableInterruptions(enable bool) error {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
|
|
d.interruptPadEnabled = enable
|
|
return d.configure()
|
|
}
|
|
|
|
// EnableParityTest controls if the sensor should control the parity of the data transmitted
|
|
func (d *Dev) EnableParityTest(enable bool) error {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
|
|
d.parityTestEnabled = enable
|
|
return d.configure()
|
|
}
|
|
|
|
// Read returns a sample from the last measurement of the sensor
|
|
func (d *Dev) Read(precision Precision) (Sample, error) {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
|
|
if precision == LowPrecision {
|
|
return d.readLowPrecision()
|
|
}
|
|
|
|
return d.readHighPrecision()
|
|
}
|
|
|
|
func (d *Dev) readLowPrecision() (Sample, error) {
|
|
// The information we need is in the first 3 registers
|
|
if err := d.i2c.Tx([]byte{registerBx}, d.registersBuffer[:numberOfFastMeasurementRegisters]); err != nil {
|
|
return Sample{}, err
|
|
}
|
|
buf := d.registersBuffer
|
|
|
|
// The values are signed:
|
|
// convert uint8 to int8, then convert to int to preserve the sign
|
|
rawBx := int(int8(buf[registerBx])) << 4
|
|
rawBy := int(int8(buf[registerBy])) << 4
|
|
rawBz := int(int8(buf[registerBz])) << 4
|
|
|
|
return Sample{
|
|
Bx: magneticFluxScaling * physic.MagneticFluxDensity(rawBx),
|
|
By: magneticFluxScaling * physic.MagneticFluxDensity(rawBy),
|
|
Bz: magneticFluxScaling * physic.MagneticFluxDensity(rawBz),
|
|
}, nil
|
|
}
|
|
|
|
func (d *Dev) readHighPrecision() (Sample, error) {
|
|
// The information we need is in the first 7 registers
|
|
if err := d.i2c.Tx([]byte{registerBx}, d.registersBuffer[:numberOfMeasurementRegisters]); err != nil {
|
|
return Sample{}, err
|
|
}
|
|
buf := d.registersBuffer
|
|
|
|
// The values are signed:
|
|
// convert uint8 to int8, then convert to int to preserve the sign
|
|
rawBx := (int(int8(buf[registerBx])) << 4) | (int(buf[registerBx2]&0xf0) >> 4)
|
|
rawBy := (int(int8(buf[registerBy])) << 4) | int(buf[registerBx2]&0x0f)
|
|
rawBz := (int(int8(buf[registerBz])) << 4) | int(buf[registerBz2]&0x0f)
|
|
rawTemp := (int(int8(buf[registerTemp]&0xf0)) << 4) | int(buf[registerTemp2])
|
|
|
|
// Compute measurement based upon reference documentation
|
|
temp := physic.Temperature(rawTemp-d.temperatureOffsetCompensation)*temperatureScaling + referenceTemperature
|
|
|
|
return Sample{
|
|
Bx: magneticFluxScaling * physic.MagneticFluxDensity(rawBx),
|
|
By: magneticFluxScaling * physic.MagneticFluxDensity(rawBy),
|
|
Bz: magneticFluxScaling * physic.MagneticFluxDensity(rawBz),
|
|
Temperature: temp,
|
|
}, nil
|
|
}
|
|
|
|
// ReadContinuous returns a channel which will receive readings at regular intervals
|
|
func (d *Dev) ReadContinuous(frequency physic.Frequency, precision Precision) (<-chan Sample, error) {
|
|
// First release the current continuous reading if there is one
|
|
d.StopContinousRead()
|
|
reading := make(chan Sample, 16)
|
|
d.stop = make(chan struct{})
|
|
|
|
// Choose the best operating mode for the sensor
|
|
newMode, err := bestModeForFrequency(frequency)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
previousMode := d.mode
|
|
if err := d.SetMode(newMode); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
t := time.NewTicker(frequency.Period())
|
|
|
|
d.continuousReadWG.Add(1)
|
|
|
|
go func(s <-chan struct{}) {
|
|
defer d.SetMode(previousMode)
|
|
defer t.Stop()
|
|
defer close(reading)
|
|
defer d.continuousReadWG.Done()
|
|
|
|
for {
|
|
select {
|
|
case <-s:
|
|
return
|
|
case <-t.C:
|
|
value, err := d.Read(precision)
|
|
if err != nil {
|
|
// In continuous mode, we'll ignore errors silently.
|
|
continue
|
|
}
|
|
reading <- value
|
|
}
|
|
}
|
|
}(d.stop)
|
|
|
|
return reading, nil
|
|
}
|
|
|
|
// StopContinousRead stops a currently running continuous read
|
|
func (d *Dev) StopContinousRead() {
|
|
if d.stop == nil {
|
|
return
|
|
}
|
|
|
|
d.stop <- struct{}{}
|
|
d.stop = nil
|
|
d.continuousReadWG.Wait()
|
|
}
|
|
|
|
// CalibrateTemperatureOffsetCompensation computes the temperature offset compensation based upon a reading from the sensor compared to the actual temperature.
|
|
// The returned value must be stored and passed in Opts.TemperatureOffsetCompensation in future uses of TLV493D driver.
|
|
// See TLV493D user manual, "3.2 Calculation of the temperature" for more information
|
|
func CalibrateTemperatureOffsetCompensation(temperatureOffsetCompensation int, measuredTemperature physic.Temperature, actualTemperature physic.Temperature) int {
|
|
// Compute measurement based upon reference documentation
|
|
rawTemp := int((measuredTemperature-referenceTemperature)/temperatureScaling) + temperatureOffsetCompensation
|
|
// Compute the new offset by matching the raw measurement between the measure and the actual value
|
|
newOffset := rawTemp - int((actualTemperature-referenceTemperature)/temperatureScaling)
|
|
|
|
return newOffset
|
|
}
|
|
|
|
func bestModeForFrequency(frequency physic.Frequency) (Mode, error) {
|
|
|
|
allowed := []*Mode{&FastMode, &LowPowerMode, &UltraLowPowerMode}
|
|
var minAbove *Mode = nil
|
|
|
|
for _, m := range allowed {
|
|
if m.measurementFrequency >= frequency {
|
|
if minAbove == nil || minAbove.measurementFrequency > m.measurementFrequency {
|
|
minAbove = m
|
|
}
|
|
}
|
|
}
|
|
|
|
if minAbove == nil {
|
|
return PowerDownMode, fmt.Errorf("frequency too high, no mode available for %s", frequency)
|
|
}
|
|
|
|
return *minAbove, nil
|
|
}
|
|
|
|
// Sets the bit at pos in the integer n.
|
|
func setBit(n byte, pos uint, isSet bool) byte {
|
|
var bit byte = (1 << pos)
|
|
if isSet {
|
|
return n | bit
|
|
}
|
|
return n &^ bit
|
|
}
|
|
|
|
func isNumberOfBitsEven(array []byte) bool {
|
|
var accumulator byte = 0
|
|
|
|
// The keys is to use XOR
|
|
// 1 ^ 1 -> 0 (even)
|
|
// 0 ^ 1 -> 1 (odd)
|
|
// 1 ^ 0 -> 1 (odd)
|
|
// 0 ^ 0 -> 0 (even)
|
|
|
|
// Combine all bytes
|
|
for _, b := range array {
|
|
accumulator ^= b
|
|
}
|
|
|
|
// Combine adjacent bits
|
|
accumulator ^= (accumulator >> 1)
|
|
accumulator ^= (accumulator >> 2)
|
|
accumulator ^= (accumulator >> 4)
|
|
|
|
// Parity is in the LSB
|
|
return !((accumulator & 0x01) == 1)
|
|
}
|