diff --git a/experimental/devices/ina219/doc.go b/experimental/devices/ina219/doc.go new file mode 100644 index 0000000..8052977 --- /dev/null +++ b/experimental/devices/ina219/doc.go @@ -0,0 +1,17 @@ +// Copyright 2018 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 ina219 controls a Texas Instruments ina219 high side current, +// voltage and power monitor IC over an i2c bus. +// +// Calibration +// +// Calibration is recommended for accurate current and power measurements. +// Voltage measurements do not require sensor calibration. To calibrate meansure +// the actual value of the shunt resistor. +// +// Datasheet +// +// http://www.ti.com/lit/ds/symlink/ina219.pdf +package ina219 diff --git a/experimental/devices/ina219/example_test.go b/experimental/devices/ina219/example_test.go new file mode 100644 index 0000000..19fe584 --- /dev/null +++ b/experimental/devices/ina219/example_test.go @@ -0,0 +1,43 @@ +// Copyright 2018 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 ina219_test + +import ( + "fmt" + "log" + + "periph.io/x/periph/conn/i2c/i2creg" + "periph.io/x/periph/experimental/devices/ina219" + "periph.io/x/periph/host" +) + +func Example() { + // Make sure periph is initialized. + if _, err := host.Init(); err != nil { + log.Fatal(err) + } + + // Open default I²C bus. + bus, err := i2creg.Open("") + if err != nil { + log.Fatalf("failed to open I²C: %v", err) + } + defer bus.Close() + + // Create a new power sensor. + sensor, err := ina219.New(bus, &ina219.DefaultOpts) + if err != nil { + log.Fatalln(err) + } + + // Read values from sensor. + measurement, err := sensor.Sense() + + if err != nil { + log.Fatalln(err) + } + + fmt.Println(measurement) +} diff --git a/experimental/devices/ina219/ina219.go b/experimental/devices/ina219/ina219.go new file mode 100644 index 0000000..fff1f0b --- /dev/null +++ b/experimental/devices/ina219/ina219.go @@ -0,0 +1,192 @@ +// Copyright 2018 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 ina219 + +import ( + "encoding/binary" + "errors" + "fmt" + "sync" + + "periph.io/x/periph/conn/i2c" + "periph.io/x/periph/conn/mmr" + "periph.io/x/periph/conn/physic" +) + +// Opts holds the configuration options. +// +// Slave Address +// +// Depending which pins the A1, A0 pins are connected to will change the slave +// address. Default configuration is address 0x40 (both pins to GND). For a full +// address table see datasheet. +type Opts struct { + Address int + SenseResistor physic.ElectricResistance + MaxCurrent physic.ElectricCurrent +} + +// DefaultOpts is the recommended default options. +var DefaultOpts = Opts{ + Address: 0x40, + SenseResistor: 100 * physic.MilliOhm, + MaxCurrent: 3200 * physic.MilliAmpere, +} + +// New opens a handle to an ina219 sensor. +func New(bus i2c.Bus, opts *Opts) (*Dev, error) { + + i2cAddress := DefaultOpts.Address + if opts.Address != 0 { + if opts.Address < 0x40 || opts.Address > 0x4f { + return nil, errAddressOutOfRange + } + i2cAddress = opts.Address + } + + senseResistor := DefaultOpts.SenseResistor + if opts.SenseResistor != 0 { + if opts.SenseResistor < 1 { + return nil, errSenseResistorValueInvalid + } + senseResistor = opts.SenseResistor + } + + maxCurrent := DefaultOpts.MaxCurrent + if opts.MaxCurrent != 0 { + if opts.MaxCurrent < 1 { + return nil, errMaxCurrentInvalid + } + maxCurrent = opts.MaxCurrent + } + + dev := &Dev{ + m: mmr.Dev8{ + Conn: &i2c.Dev{Bus: bus, Addr: uint16(i2cAddress)}, + Order: binary.BigEndian, + }, + } + + if err := dev.calibrate(senseResistor, maxCurrent); err != nil { + return nil, err + } + + if err := dev.m.WriteUint16(configRegister, 0x1FFF); err != nil { + return nil, errWritingToConfigRegister + } + + return dev, nil +} + +// Dev is a handle to the ina219 sensor. +type Dev struct { + m mmr.Dev8 + + mu sync.Mutex + currentLSB physic.ElectricCurrent + powerLSB physic.Power +} + +const ( + configRegister = 0x00 + shuntVoltageRegister = 0x01 + busVoltageRegister = 0x02 + powerRegister = 0x03 + currentRegister = 0x04 + calibrationRegister = 0x05 +) + +// Sense reads the power values from the ina219 sensor. +func (d *Dev) Sense() (PowerMonitor, error) { + d.mu.Lock() + defer d.mu.Unlock() + + var pm PowerMonitor + + shunt, err := d.m.ReadUint16(shuntVoltageRegister) + if err != nil { + return PowerMonitor{}, errReadShunt + } + // Least significant bit is 10µV. + pm.Shunt = physic.ElectricPotential(shunt) * 10 * physic.MicroVolt + + bus, err := d.m.ReadUint16(busVoltageRegister) + if err != nil { + return PowerMonitor{}, errReadBus + } + // Check if bit zero is set, if set the ADC has overflowed. + if bus&1 > 0 { + return PowerMonitor{}, errRegisterOverflow + } + + // Least significant bit is 4mV. + pm.Voltage = physic.ElectricPotential(bus>>3) * 4 * physic.MilliVolt + + current, err := d.m.ReadUint16(currentRegister) + if err != nil { + return PowerMonitor{}, errReadCurrent + } + pm.Current = physic.ElectricCurrent(current) * d.currentLSB + + power, err := d.m.ReadUint16(powerRegister) + if err != nil { + return PowerMonitor{}, errReadPower + } + pm.Power = physic.Power(power) * d.powerLSB + + return pm, nil +} + +// Since physic electrical is in nano units we need to scale taking care to not +// overflow int64 or loose resolution. +const calibratescale int64 = ((int64(physic.Ampere) * int64(physic.Ohm)) / 100000) << 12 + +// calibrate sets the scaling factor of the current and power registers for the +// maximum resolution. calibrate is run on init. +func (d *Dev) calibrate(sense physic.ElectricResistance, maxCurrent physic.ElectricCurrent) error { + // TODO: Check calibration with float implementation in tests. + if sense <= 0 { + return errSenseResistorValueInvalid + } + if maxCurrent <= 0 { + return errMaxCurrentInvalid + } + + d.mu.Lock() + defer d.mu.Unlock() + + d.currentLSB = maxCurrent / (2 << 15) + d.powerLSB = physic.Power(d.currentLSB * 20) + // Calibration Register = 0.04096 / (current LSB * Shunt Resistance) + // Where lsb is in Amps and resistance is in ohms. + // Calibration register is 16 bits. + cal := uint16(calibratescale / (int64(d.currentLSB) * int64(sense))) + return d.m.WriteUint16(calibrationRegister, cal) +} + +// PowerMonitor represents measurements from ina219 sensor. +type PowerMonitor struct { + Shunt physic.ElectricPotential + Voltage physic.ElectricPotential + Current physic.ElectricCurrent + Power physic.Power +} + +// String returns a PowerMonitor as string +func (p PowerMonitor) String() string { + return fmt.Sprintf("Bus: %s, Current: %s, Power: %s, Shunt: %s", p.Voltage, p.Current, p.Power, p.Shunt) +} + +var ( + errReadShunt = errors.New("failed to read shunt voltage") + errReadBus = errors.New("failed to read bus voltage") + errReadPower = errors.New("failed to read power") + errReadCurrent = errors.New("failed to read current") + errAddressOutOfRange = errors.New("i2c address out of range") + errSenseResistorValueInvalid = errors.New("sense resistor value cannot be negative or zero") + errMaxCurrentInvalid = errors.New("max current cannot be negative or zero") + errRegisterOverflow = errors.New("bus voltage register overflow") + errWritingToConfigRegister = errors.New("failed to write to configuration register") +) diff --git a/experimental/devices/ina219/ina219_test.go b/experimental/devices/ina219/ina219_test.go new file mode 100644 index 0000000..02e84ce --- /dev/null +++ b/experimental/devices/ina219/ina219_test.go @@ -0,0 +1,394 @@ +// Copyright 2018 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 ina219 + +import ( + "encoding/binary" + "errors" + "strings" + "testing" + + "periph.io/x/periph/conn/i2c" + "periph.io/x/periph/conn/i2c/i2ctest" + "periph.io/x/periph/conn/mmr" + "periph.io/x/periph/conn/physic" +) + +func TestNew(t *testing.T) { + stringErr := errors.New("use err.Error() error") + + type fields struct { + currentLSB physic.ElectricCurrent + powerLSB physic.Power + } + + var tests = []struct { + name string + opts Opts + want fields + tx []i2ctest.IO + err error + errString string + }{ + {name: "defaults", + tx: []i2ctest.IO{ + {Addr: 0x40, W: []byte{calibrationRegister, 0x20, 0xc4}, R: []byte{}}, + {Addr: 0x40, W: []byte{configRegister, 0x1f, 0xff}, R: []byte{}}, + }, + want: fields{ + currentLSB: 48828 * physic.NanoAmpere, + powerLSB: 976560 * physic.NanoWatt, + }, + }, + {name: "badAddressOption", + opts: Opts{Address: 0x60}, + err: errAddressOutOfRange, + }, + {name: "badSenseResistorOption", + opts: Opts{SenseResistor: -1}, + err: errSenseResistorValueInvalid, + }, + {name: "badMaxCurrentOption", + opts: Opts{MaxCurrent: -1}, + err: errMaxCurrentInvalid, + }, + {name: "setAddress", + opts: Opts{Address: 0x41}, + tx: []i2ctest.IO{ + {Addr: 0x41, W: []byte{calibrationRegister, 0x20, 0xc4}, R: []byte{}}, + {Addr: 0x41, W: []byte{configRegister, 0x1f, 0xff}, R: []byte{}}, + }, + want: fields{ + currentLSB: 48828 * physic.NanoAmpere, + powerLSB: 976560 * physic.NanoWatt, + }, + err: nil, + }, + {name: "setMaxCurrent", + opts: Opts{MaxCurrent: 1000 * physic.MilliAmpere}, + tx: []i2ctest.IO{ + {Addr: 0x40, W: []byte{calibrationRegister, 0x68, 0xdc}, R: []byte{}}, + {Addr: 0x40, W: []byte{configRegister, 0x1f, 0xff}, R: []byte{}}, + }, + want: fields{ + currentLSB: 15258 * physic.NanoAmpere, + powerLSB: 305160 * physic.NanoWatt, + }, + err: nil, + }, + {name: "setSenseResistor", + opts: Opts{SenseResistor: 10 * physic.MilliOhm}, + tx: []i2ctest.IO{ + {Addr: 0x40, W: []byte{calibrationRegister, 0x47, 0xae}, R: []byte{}}, + {Addr: 0x40, W: []byte{configRegister, 0x1f, 0xff}, R: []byte{}}, + }, + want: fields{ + currentLSB: 48828 * physic.NanoAmpere, + powerLSB: 976560 * physic.NanoWatt, + }, + err: nil, + }, + {name: "txError", + tx: []i2ctest.IO{{Addr: 0x40, W: []byte{}, R: []byte{}}}, + want: fields{ + currentLSB: 48828 * physic.NanoAmpere, + powerLSB: 976560 * physic.NanoWatt, + }, + err: stringErr, + errString: "unexpected write", + }, + {name: "errWritingToConfigRegister", + tx: []i2ctest.IO{ + {Addr: 0x40, W: []byte{calibrationRegister, 0x20, 0xc4}, R: []byte{}}, + {Addr: 0x40, W: []byte{configRegister}, R: []byte{}}, + }, + want: fields{ + currentLSB: 48828 * physic.NanoAmpere, + powerLSB: 976560 * physic.NanoWatt, + }, + err: errWritingToConfigRegister, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + + bus := &i2ctest.Playback{ + Ops: test.tx, + DontPanic: true, + } + + ina, err := New(bus, &test.opts) + + if test.err != nil { + if err != test.err { + if test.err == stringErr { + if !strings.Contains(err.Error(), test.errString) { + t.Errorf("%v wanted err: %v, but got: %v", test.name, test.errString, err) + } + } else { + t.Errorf("%v wanted err: %v, but got: %v", test.name, test.err, err) + } + } + } + + if test.err == nil { + if ina == nil { + t.Errorf("%v wanted no err but got: %v", test.name, err) + return + } + + var got = fields{ + currentLSB: ina.currentLSB, + powerLSB: ina.powerLSB, + } + if got != test.want { + t.Errorf("%v wanted: %v, but got: %v", test.name, test.want, got) + } + + } + + }) + } + +} + +func TestSense(t *testing.T) { + stringErr := errors.New("use err.Error() error") + type fields struct { + currentLSB physic.ElectricCurrent + powerLSB physic.Power + } + + var tests = []struct { + name string + args Opts + want PowerMonitor + tx []i2ctest.IO + err error + errString string + }{ + { + name: "errReadShunt", + err: errReadShunt, + args: Opts{}, + tx: []i2ctest.IO{ + {Addr: 0x40, W: []byte{calibrationRegister, 0x20, 0xc4}, R: []byte{}}, + {Addr: 0x40, W: []byte{configRegister, 0x1f, 0xff}, R: []byte{}}, + {Addr: 0x40, W: []byte{shuntVoltageRegister}, R: []byte{}}, + }, + }, + { + name: "errReadBus", + err: errReadBus, + args: Opts{}, + tx: []i2ctest.IO{ + {Addr: 0x40, W: []byte{calibrationRegister, 0x20, 0xc4}, R: []byte{}}, + {Addr: 0x40, W: []byte{configRegister, 0x1f, 0xff}, R: []byte{}}, + {Addr: 0x40, W: []byte{shuntVoltageRegister}, R: []byte{0x00, 0x00}}, + {Addr: 0x40, W: []byte{busVoltageRegister}, R: []byte{}}, + }, + }, + { + name: "errReadCurrent", + err: errReadCurrent, + args: Opts{}, + tx: []i2ctest.IO{ + {Addr: 0x40, W: []byte{calibrationRegister, 0x20, 0xc4}, R: []byte{}}, + {Addr: 0x40, W: []byte{configRegister, 0x1f, 0xff}, R: []byte{}}, + {Addr: 0x40, W: []byte{shuntVoltageRegister}, R: []byte{0x00, 0x00}}, + {Addr: 0x40, W: []byte{busVoltageRegister}, R: []byte{0x00, 0x00}}, + {Addr: 0x40, W: []byte{currentRegister}, R: []byte{}}, + }, + }, + { + name: "errReadPower", + err: errReadPower, + args: Opts{}, + tx: []i2ctest.IO{ + {Addr: 0x40, W: []byte{calibrationRegister, 0x20, 0xc4}, R: []byte{}}, + {Addr: 0x40, W: []byte{configRegister, 0x1f, 0xff}, R: []byte{}}, + {Addr: 0x40, W: []byte{shuntVoltageRegister}, R: []byte{0x00, 0x00}}, + {Addr: 0x40, W: []byte{busVoltageRegister}, R: []byte{0x00, 0x00}}, + {Addr: 0x40, W: []byte{currentRegister}, R: []byte{0x00, 0x00}}, + {Addr: 0x40, W: []byte{powerRegister}, R: []byte{}}, + }, + }, + { + name: "readZero", + err: nil, + args: Opts{}, + tx: []i2ctest.IO{ + {Addr: 0x40, W: []byte{calibrationRegister, 0x20, 0xc4}, R: []byte{}}, + {Addr: 0x40, W: []byte{configRegister, 0x1f, 0xff}, R: []byte{}}, + {Addr: 0x40, W: []byte{shuntVoltageRegister}, R: []byte{0x00, 0x00}}, + {Addr: 0x40, W: []byte{busVoltageRegister}, R: []byte{0x00, 0x00}}, + {Addr: 0x40, W: []byte{currentRegister}, R: []byte{0x00, 0x00}}, + {Addr: 0x40, W: []byte{powerRegister}, R: []byte{0x00, 0x00}}, + }, + want: PowerMonitor{Shunt: 0, Voltage: 0, Current: 0, Power: 0}, + }, + { + name: "busVoltageOverflow", + err: errRegisterOverflow, + args: Opts{}, + tx: []i2ctest.IO{ + {Addr: 0x40, W: []byte{calibrationRegister, 0x20, 0xc4}, R: []byte{}}, + {Addr: 0x40, W: []byte{configRegister, 0x1f, 0xff}, R: []byte{}}, + {Addr: 0x40, W: []byte{shuntVoltageRegister}, R: []byte{0x00, 0x00}}, + {Addr: 0x40, W: []byte{busVoltageRegister}, R: []byte{0x00, 0x01}}, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + bus := &i2ctest.Playback{ + Ops: test.tx, + DontPanic: true, + } + ina, err := New(bus, &Opts{}) + if err != nil { + t.Fatalf("set setup failure %v", err) + } + if ina == nil { + t.Fatalf("device init failed") + } + got, err := ina.Sense() + if test.err != nil { + if err != test.err { + if test.err == stringErr { + if !strings.Contains(err.Error(), test.errString) { + t.Errorf("%v wanted err: %v, but got: %v", test.name, test.errString, err) + } + } else { + t.Errorf("%v wanted err: %v, but got: %v", test.name, test.err, err) + } + } + } + + if test.err == nil { + if err != nil { + t.Errorf("%v wanted no err but got: %v", test.name, err) + return + } + + if got != test.want { + t.Errorf("%v wanted: %v, but got: %v", test.name, test.want, got) + } + + } + }) + } +} + +func TestCalibrate(t *testing.T) { + stringErr := errors.New("use err.Error() error") + + type fields struct { + sense physic.ElectricResistance + maxCurrent physic.ElectricCurrent + currentLSB physic.ElectricCurrent + powerLSB physic.Power + caibrated bool + } + tests := []struct { + name string + tx []i2ctest.IO + args fields + want fields + err error + errString string + }{ + { + name: "errBadSense", + err: errSenseResistorValueInvalid, + }, + { + name: "errBadMaxCurrent", + args: fields{ + sense: physic.MilliOhm, + }, + err: errMaxCurrentInvalid, + }, + { + name: "errIO", + args: fields{ + sense: physic.MilliOhm, + maxCurrent: physic.Ampere, + }, + err: stringErr, + errString: "unexpected Tx", + }, + { + name: "default", + args: fields{ + sense: 100 * physic.MilliOhm, + maxCurrent: 3200 * physic.MilliAmpere, + }, + want: fields{ + currentLSB: 48828 * physic.NanoAmpere, + powerLSB: 976560 * physic.NanoWatt, + }, + tx: []i2ctest.IO{ + {Addr: 0x40, W: []byte{0x05, 0x20, 0xc4}, R: []byte{}}, + }, + err: nil, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + + bus := i2ctest.Playback{ + Ops: test.tx, + DontPanic: true, + } + + ina := &Dev{ + m: mmr.Dev8{ + Conn: &i2c.Dev{Bus: &bus, Addr: 0x40}, + Order: binary.BigEndian}, + } + + err := ina.calibrate(test.args.sense, test.args.maxCurrent) + if test.err != nil { + if err != test.err { + if test.err == stringErr { + if !strings.Contains(err.Error(), test.errString) { + t.Errorf("%v wanted err: %v, but got: %v", test.name, test.errString, err) + } + } else { + t.Errorf("%v wanted err: %v, but got: %v", test.name, test.err, err) + } + } + } + if test.err == nil { + if err != nil { + t.Errorf("%v wanted no err but got: %v", test.name, err) + } + got := fields{ + currentLSB: ina.currentLSB, + powerLSB: ina.powerLSB, + } + if got != test.want { + t.Errorf("%v wanted: %v, but got: %v", test.name, test.want, got) + } + } + }) + } +} + +func TestPowerStringer(t *testing.T) { + var p = PowerMonitor{ + Shunt: 1, + Voltage: 1, + Current: 1, + Power: 1, + } + want := "Bus: 1nV, Current: 1nA, Power: 1nW, Shunt: 1nV" + got := p.String() + if want != got { + t.Errorf("wanted %s\n, but got: %s", want, got) + } +} diff --git a/experimental/devices/ina219/ina219smoketest/ina219smoketest.go b/experimental/devices/ina219/ina219smoketest/ina219smoketest.go new file mode 100644 index 0000000..6e945e5 --- /dev/null +++ b/experimental/devices/ina219/ina219smoketest/ina219smoketest.go @@ -0,0 +1,77 @@ +// Copyright 2018 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 ina219smoketest + +import ( + "errors" + "flag" + "fmt" + + "periph.io/x/periph/conn/i2c/i2creg" + "periph.io/x/periph/conn/physic" + "periph.io/x/periph/experimental/devices/ina219" + "periph.io/x/periph/host" +) + +// SmokeTest is imported by periph-smoketest. +type SmokeTest struct { +} + +// Name implements the SmokeTest interface. +func (s *SmokeTest) Name() string { + return "ina219" +} + +// Description implements the SmokeTest interface. +func (s *SmokeTest) Description() string { + return "Tests INA219 over I²C" +} + +func (s *SmokeTest) Run(f *flag.FlagSet, args []string) (err error) { + i2cID := f.String("i2c", "", "I²C bus to use") + i2cAddr := f.Int("ia", 0x40, "I²C bus address use: 0x40 to 0x4f") + if err := f.Parse(args); err != nil { + return err + } + if f.NArg() != 0 { + f.Usage() + return errors.New("unrecognized arguments") + } + + fmt.Println("Starting INA219 Current Sensor\nctrl+c to exit") + if _, err := host.Init(); err != nil { + return err + } + + // Open default i2c bus. + bus, err := i2creg.Open(*i2cID) + if err != nil { + return err + } + defer func() { + if err2 := bus.Close(); err == nil { + err = err2 + } + }() + + // Create a new power sensor a sense resistor of 100 mΩ. + config := &ina219.Opts{ + Address: *i2cAddr, + SenseResistor: 100 * physic.MilliOhm, + MaxCurrent: 3200 * physic.MilliAmpere, + } + + sensor, err := ina219.New(bus, config) + if err != nil { + return err + } + pm, err := sensor.Sense() + if err != nil { + return err + } + fmt.Println(pm) + + return nil +}