ina219: add i2c driver for high side current and voltage sensor (#292)

pull/1/head
NeuralSpaz 8 years ago committed by M-A
parent 5d01024987
commit f54b53f207

@ -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

@ -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)
}

@ -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")
)

@ -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)
}
}

@ -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
}
Loading…
Cancel
Save