Add I2C INA219 driver (#705)

INA219: add INA219 driver
This commit is contained in:
Warren Guy
2024-10-24 06:45:33 +01:00
committed by GitHub
parent f308f8fce0
commit 6d431e0726
7 changed files with 673 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
package main
import (
"machine"
"time"
"tinygo.org/x/drivers/ina219"
)
func main() {
machine.I2C0.Configure(machine.I2CConfig{})
dev := ina219.New(machine.I2C0)
dev.Configure()
for {
busVoltage, shuntVoltage, current, power, err := dev.Measurements()
if err != nil {
println("Error reading measurements", err)
}
println("Bus Voltage:", busVoltage, "V")
println("Shunt Voltage:", shuntVoltage/100, "mV")
println("Current:", current, "mA")
println("Power:", power, "mW")
time.Sleep(10 * time.Millisecond)
}
}
+183
View File
@@ -0,0 +1,183 @@
package ina219
type Config struct {
// BusVoltageRange sets the bus voltage range.
BusVoltageRange BusVoltageRange
// PGA sets the programmable gain amplifier.
PGA PGA
// BusADC sets the bus ADC resolution.
BusADC BusADC
// ShuntADC sets the shunt ADC resolution.
ShuntADC ShuntADC
// Mode sets the operating mode.
Mode Mode
// Calibration sets the calibration value for the expected
// voltage and current values.
Calibration Calibration
// 1000 / uA per bit
CurrentDivider float32
// 1mW per bit
PowerMultiplier float32
}
// RegisterValue returns the register value of the configuration.
func (c *Config) RegisterValue() uint16 {
return c.BusVoltageRange.RegisterValue() |
c.PGA.RegisterValue() |
c.BusADC.RegisterValue() |
c.ShuntADC.RegisterValue() |
c.Mode.RegisterValue()
}
// Generate a new configuration from a register value.
func NewConfig(config int16, calibration int16) Config {
return Config{
BusVoltageRange: BusVoltageRange(config >> 13 & 0x1),
PGA: PGA(config >> 11 & 0x3),
BusADC: BusADC(config >> 7 & 0xF),
ShuntADC: ShuntADC(config >> 3 & 0xF),
Mode: Mode(config & 0x7),
Calibration: Calibration(calibration),
}
}
// Configurations from
// https://github.com/adafruit/Adafruit_INA219/blob/master/Adafruit_INA219.cpp
var (
// Config32V2A is a configuration for a 32V 2A range.
Config32V2A = Config{
BusVoltageRange: Range32V,
PGA: PGA8,
BusADC: ADC12,
ShuntADC: SADC12,
Mode: ModeContShuntBus,
Calibration: Calibration16V400mA,
CurrentDivider: 10.0,
PowerMultiplier: 2.0,
}
// Config32V1A is a configuration for a 32V 1A range.
Config32V1A = Config{
BusVoltageRange: Range32V,
PGA: PGA8,
BusADC: ADC12,
ShuntADC: SADC12,
Mode: ModeContShuntBus,
Calibration: Calibration32V1A,
CurrentDivider: 25.0,
PowerMultiplier: 0.8,
}
// Config16V400mA is a configuration for a 16V 400mA range.
Config16V400mA = Config{
BusVoltageRange: Range16V,
PGA: PGA1,
BusADC: ADC12,
ShuntADC: SADC12,
Mode: ModeContShuntBus,
Calibration: Calibration16V400mA,
CurrentDivider: 20.0,
PowerMultiplier: 1.0,
}
)
// BusVoltageRange is the bus voltage range.
type BusVoltageRange int8
const (
Range16V BusVoltageRange = 0 // 0-16V
Range32V BusVoltageRange = 1 // 0-32V
)
func (r BusVoltageRange) RegisterValue() uint16 {
return uint16(r) << 13
}
// PGA is the programmable gain amplifier.
type PGA int8
const (
PGA1 PGA = 0 // 40mV
PGA2 PGA = 1 // 80mV
PGA4 PGA = 2 // 160mV
PGA8 PGA = 3 // 320mV
)
func (p PGA) RegisterValue() uint16 {
return uint16(p) << 11
}
// BusADC is the bus ADC resolution.
type BusADC int8
const (
ADC9 BusADC = 0 // 9-bit
ADC10 BusADC = 1 // 10-bit
ADC11 BusADC = 2 // 11-bit
ADC12 BusADC = 3 // 12-bit
)
func (b BusADC) RegisterValue() uint16 {
return uint16(b) << 7
}
// ShuntADC is the shunt ADC resolution.
type ShuntADC int8
const (
SADC9 ShuntADC = 0 // 9-bit
SADC10 ShuntADC = 1 // 10-bit
SADC11 ShuntADC = 2 // 11-bit
SADC12 ShuntADC = 3 // 12-bit
)
func (s ShuntADC) RegisterValue() uint16 {
return uint16(s) << 3
}
// Mode is the operating mode.
type Mode int8
const (
ModePowerDown Mode = 0 // power-down
ModeTrigShunt Mode = 1 // triggered shunt voltage
ModeTrigBus Mode = 2 // triggered bus voltage
ModeTrigShuntBus Mode = 3 // triggered shunt and bus voltage
ModeADCOff Mode = 4 // ADC off
ModeContShunt Mode = 5 // continuous shunt voltage
ModeContBus Mode = 6 // continuous bus voltage
ModeContShuntBus Mode = 7 // continuous shunt and bus voltage
)
// ModeTriggered is a mask for triggered modes.
const ModeTriggeredMask Mode = 0x4
// ModeTriggered returns true if the mode is a triggered mode.
func ModeTriggered(m Mode) bool {
return m != ModePowerDown && m&ModeTriggeredMask == 0
}
func (m Mode) RegisterValue() uint16 {
return uint16(m)
}
// Calibration is the calibration register for the INA219. Values from:
// https://github.com/adafruit/Adafruit_INA219/blob/master/Adafruit_INA219.cpp
type Calibration uint16
const (
Calibration32V2A Calibration = 4096
Calibration32V1A Calibration = 10240
Calibration16V400mA Calibration = 8192
)
func (c Calibration) RegisterValue() uint16 {
return uint16(c)
}
+13
View File
@@ -0,0 +1,13 @@
package ina219
type ErrOverflow struct{}
func (e ErrOverflow) Error() string { return "overflow" }
type ErrNotReady struct{}
func (e ErrNotReady) Error() string { return "not ready" }
type ErrConfigMismatch struct{}
func (e ErrConfigMismatch) Error() string { return "config mismatch" }
+202
View File
@@ -0,0 +1,202 @@
package ina219
import (
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/internal/legacy"
)
// An INA219 device.
type Device struct {
bus drivers.I2C
Address uint16
config Config
}
// Create a new INA219 device with the default configuration
// and the given I2C bus at the default address.
//
// Set Address after New to change the address.
//
// Call Configure after New to write the configuration to the
// device. If you don't call Configure, the device may have a
// different configuration and the power divider and current
// multiplier are probably wrong.
func New(bus drivers.I2C) Device {
return Device{
bus: bus,
Address: Address,
config: Config32V2A,
}
}
// Set the configuration for the device. This only changes the
// configuration in memory, not on the device. Call Configure
// to write the configuration to the device.
func (d *Device) SetConfig(config Config) {
d.config = config
}
// Write the current configuration to the device.
func (d *Device) Configure() (err error) {
if err = d.WriteRegister(
RegConfig,
d.config.RegisterValue(),
); err != nil {
return
}
if err = d.WriteRegister(
RegCalibration,
d.config.Calibration.RegisterValue(),
); err != nil {
return
}
var readConfig Config
// make sure the configuration is read back correctly
if readConfig, err = d.ReadConfig(); err != nil {
return
} else if readConfig.RegisterValue() != d.config.RegisterValue() {
err = ErrConfigMismatch{}
} else if readConfig.Calibration.RegisterValue() != d.config.Calibration.RegisterValue() {
err = ErrConfigMismatch{}
}
return
}
// Trigger a conversion. This is only necessary if the device is in
// trigger mode. In continuous mode (the default), the device will
// automatically trigger conversions and this has no effect. See
// config.go.
//
// Triggering a conversion or reading the "power" register resets
// the conversion ready bit.
func (d *Device) Trigger() (err error) {
// Only trigger if the mode is one of the triggered modes.
if ModeTriggered(d.config.Mode) {
err = d.WriteRegister(RegConfig, d.config.RegisterValue())
}
return
}
// Measurements reads the bus voltage, shunt voltage, current, and power
// from the device.
func (d *Device) Measurements() (
busVoltage int16,
shuntVoltage int16,
current float32,
power float32,
err error,
) {
// Attempt to read bus voltage first, so we can check for overflow
// or conversion not ready.
if busVoltage, err = d.BusVoltage(); err != nil {
return
}
// Read the rest of the values, reading Power last, which resets
// the conversion ready bit (relevant for triggered modes).
if shuntVoltage, err = d.ShuntVoltage(); err != nil {
return
}
if current, err = d.Current(); err != nil {
return
}
if power, err = d.Power(); err != nil {
return
}
return
}
// BusVoltage reads the "bus" voltage in millivolts.
//
// It returns an error if the value is invalid due to overflow
// or if the conversion is not ready yet. In a continuous mode
// there should always be a measurement available after the
// device is ready. See above notes on Trigger.
func (d *Device) BusVoltage() (voltage int16, err error) {
val, err := d.ReadRegister(RegBusVoltage)
if err != nil {
return
}
// The overflow bit is set, so the values are invalid.
if val&(1<<0) != 0 {
err = ErrOverflow{}
return
}
// The conversion is not ready yet.
if ModeTriggered(d.config.Mode) && val&(1<<1) != 0 {
err = ErrNotReady{}
return
}
voltage = (int16(val) >> 3) * 4
return
}
// ShuntVoltage reads the "shunt" voltage in 100ths of a millivolt.
func (d *Device) ShuntVoltage() (voltage int16, err error) {
return d.ReadRegister(RegShuntVoltage)
}
// Current reads the current in milliamps.
func (d *Device) Current() (current float32, err error) {
val, err := d.ReadRegister(RegCurrent)
if err != nil {
return
}
current = float32(val) / d.config.CurrentDivider
return
}
// Power reads the power in milliwatts.
func (d *Device) Power() (power float32, err error) {
val, err := d.ReadRegister(RegPower)
if err != nil {
return
}
power = float32(val) * d.config.PowerMultiplier
return
}
// Read the configuration from the device.
func (d *Device) ReadConfig() (config Config, err error) {
var cfg, cal int16
if cfg, err = d.ReadRegister(RegConfig); err != nil {
return
}
if cal, err = d.ReadRegister(RegCalibration); err != nil {
return
}
config = NewConfig(cfg, cal)
return
}
// Read a register from the device.
func (d *Device) ReadRegister(reg uint8) (val int16, err error) {
buf := make([]byte, 2)
err = legacy.ReadRegister(d.bus, uint8(d.Address), reg, buf)
if err != nil {
return
}
val = int16(buf[0])<<8 | int16(buf[1]&0xff)
return
}
// Write to a register on the device.
func (d *Device) WriteRegister(reg uint8, val uint16) error {
buf := []byte{byte(val >> 8), byte(val & 0xff)}
return legacy.WriteRegister(d.bus, uint8(d.Address), reg, buf)
}
+232
View File
@@ -0,0 +1,232 @@
package ina219
import (
"fmt"
"testing"
qt "github.com/frankban/quicktest"
"tinygo.org/x/drivers/tester"
)
func TestDefaultAddress(t *testing.T) {
c := qt.New(t)
bus := tester.NewI2CBus(c)
dev := New(bus)
c.Assert(dev.Address, qt.Equals, uint16(Address))
}
func TestBusVoltage(t *testing.T) {
t.Run("valid", func(t *testing.T) {
c := qt.New(t)
bus := tester.NewI2CBus(c)
fake := tester.NewI2CDevice16(c, Address)
fake.Registers = map[uint8]uint16{
RegBusVoltage: (4200 << 3) / 4, // 4.2V
}
bus.AddDevice(fake)
dev := New(bus)
voltage, err := dev.BusVoltage()
c.Assert(err, qt.IsNil)
c.Assert(voltage, qt.Equals, int16(4200))
})
t.Run("overflow", func(t *testing.T) {
c := qt.New(t)
bus := tester.NewI2CBus(c)
fake := tester.NewI2CDevice16(c, Address)
fake.Registers = map[uint8]uint16{
RegBusVoltage: (1 >> 0), // overflow
}
bus.AddDevice(fake)
dev := New(bus)
_, err := dev.BusVoltage()
c.Assert(err, qt.Not(qt.IsNil))
c.Assert(err, qt.ErrorMatches, ErrOverflow{}.Error())
})
t.Run("not ready", func(t *testing.T) {
c := qt.New(t)
bus := tester.NewI2CBus(c)
fake := tester.NewI2CDevice16(c, Address)
fake.Registers = map[uint8]uint16{
RegBusVoltage: ((4200 << 3) / 4) | (1 << 1), // not ready
}
bus.AddDevice(fake)
dev := New(bus)
dev.config.Mode = ModeTrigBus
_, err := dev.BusVoltage()
c.Assert(err, qt.Not(qt.IsNil))
c.Assert(err, qt.ErrorMatches, ErrNotReady{}.Error())
})
}
func TestShuntVoltage(t *testing.T) {
c := qt.New(t)
bus := tester.NewI2CBus(c)
fake := tester.NewI2CDevice16(c, Address)
fake.Registers = map[uint8]uint16{
RegShuntVoltage: 0x1234,
}
bus.AddDevice(fake)
dev := New(bus)
voltage, err := dev.ShuntVoltage()
c.Assert(err, qt.IsNil)
c.Assert(voltage, qt.Equals, int16(0x1234))
}
func TestCurrent(t *testing.T) {
c := qt.New(t)
bus := tester.NewI2CBus(c)
fake := tester.NewI2CDevice16(c, Address)
fake.Registers = map[uint8]uint16{
RegCurrent: 420 * 6.9, // 420mA
}
bus.AddDevice(fake)
dev := New(bus)
dev.config.CurrentDivider = 6.9
current, err := dev.Current()
c.Assert(err, qt.IsNil)
c.Assert(current, qt.Equals, float32(420))
}
func TestPower(t *testing.T) {
c := qt.New(t)
bus := tester.NewI2CBus(c)
fake := tester.NewI2CDevice16(c, Address)
fake.Registers = map[uint8]uint16{
RegPower: 420 / 0.8, // 420mW
}
bus.AddDevice(fake)
dev := New(bus)
dev.config.PowerMultiplier = 0.8
power, err := dev.Power()
c.Assert(err, qt.IsNil)
c.Assert(power, qt.Equals, float32(420))
}
func TestReadConfig(t *testing.T) {
// use the default configurations
for _, tc := range []Config{
Config16V400mA,
Config32V2A,
Config32V1A,
} {
n := fmt.Sprintf("%x/%x", tc.RegisterValue(), tc.Calibration.RegisterValue())
t.Run(n, func(t *testing.T) {
c := qt.New(t)
bus := tester.NewI2CBus(c)
fake := tester.NewI2CDevice16(c, Address)
fake.Registers = map[uint8]uint16{
RegConfig: tc.RegisterValue(),
RegCalibration: tc.Calibration.RegisterValue(),
}
bus.AddDevice(fake)
dev := New(bus)
config, err := dev.ReadConfig()
c.Assert(err, qt.IsNil)
c.Assert(config.BusADC, qt.Equals, tc.BusADC)
c.Assert(config.BusVoltageRange, qt.Equals, tc.BusVoltageRange)
c.Assert(config.Calibration, qt.Equals, tc.Calibration)
c.Assert(config.Mode, qt.Equals, tc.Mode)
c.Assert(config.PGA, qt.Equals, tc.PGA)
c.Assert(config.ShuntADC, qt.Equals, tc.ShuntADC)
})
}
}
func TestWriteConfig(t *testing.T) {
// use the default configurations
for _, tc := range []Config{
Config16V400mA,
Config32V2A,
Config32V1A,
} {
n := fmt.Sprintf("%x/%x", tc.RegisterValue(), tc.Calibration.RegisterValue())
t.Run(n, func(t *testing.T) {
c := qt.New(t)
bus := tester.NewI2CBus(c)
fake := tester.NewI2CDevice16(c, Address)
bus.AddDevice(fake)
fake.Registers = map[uint8]uint16{
RegConfig: 0,
RegCalibration: 0,
}
dev := New(bus)
dev.config = tc
err := dev.Configure()
c.Assert(err, qt.IsNil)
c.Assert(fake.Registers[RegConfig], qt.Equals, tc.RegisterValue())
c.Assert(fake.Registers[RegCalibration], qt.Equals, tc.Calibration.RegisterValue())
})
}
}
func TestSetConfig(t *testing.T) {
for _, tc := range []Config{
Config16V400mA,
Config32V2A,
Config32V1A,
} {
n := fmt.Sprintf("%x/%x", tc.RegisterValue(), tc.Calibration.RegisterValue())
t.Run(n, func(t *testing.T) {
c := qt.New(t)
bus := tester.NewI2CBus(c)
dev := New(bus)
dev.SetConfig(tc)
c.Assert(dev.config, qt.Equals, tc)
})
}
}
func TestTrigger(t *testing.T) {
c := qt.New(t)
bus := tester.NewI2CBus(c)
fake := tester.NewI2CDevice16(c, Address)
bus.AddDevice(fake)
fake.Registers = map[uint8]uint16{
RegConfig: Config32V2A.RegisterValue(),
}
dev := New(bus)
dev.config = Config32V2A
dev.config.Mode = ModeTrigBus
err := dev.Trigger()
c.Assert(err, qt.IsNil)
c.Assert(fake.Registers[RegConfig], qt.Equals, dev.config.RegisterValue())
}
func TestMeasurements(t *testing.T) {
bvVal := int16(4200)
svVal := int16(1234)
iVal := float32(420)
pVal := float32(420)
c := qt.New(t)
bus := tester.NewI2CBus(c)
fake := tester.NewI2CDevice16(c, Address)
bus.AddDevice(fake)
fake.Registers = map[uint8]uint16{
RegBusVoltage: uint16(((4200 << 3) / 4) | (1 << 1)),
RegShuntVoltage: uint16(svVal),
RegCurrent: uint16(iVal * Config16V400mA.CurrentDivider),
RegPower: uint16(pVal / Config16V400mA.PowerMultiplier),
}
dev := New(bus)
dev.config = Config16V400mA
bv, sv, i, p, err := dev.Measurements()
c.Assert(err, qt.IsNil)
c.Assert(bv, qt.Equals, bvVal)
c.Assert(sv, qt.Equals, svVal)
c.Assert(i, qt.Equals, iVal)
c.Assert(p, qt.Equals, pVal)
}
+13
View File
@@ -0,0 +1,13 @@
package ina219
// The default I2C address for this device.
const Address = 0x40
const (
RegConfig uint8 = 0x0
RegShuntVoltage uint8 = 0x1
RegBusVoltage uint8 = 0x2
RegPower uint8 = 0x3
RegCurrent uint8 = 0x4
RegCalibration uint8 = 0x5
)
+1
View File
@@ -109,6 +109,7 @@ tinygo build -size short -o ./build/test.hex -target=xiao ./examples/pcf8563/tim
tinygo build -size short -o ./build/test.hex -target=pico ./examples/qmi8658c/main.go tinygo build -size short -o ./build/test.hex -target=pico ./examples/qmi8658c/main.go
tinygo build -size short -o ./build/test.hex -target=feather-rp2040 ./examples/pcf8591/ tinygo build -size short -o ./build/test.hex -target=feather-rp2040 ./examples/pcf8591/
tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/ina260/main.go tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/ina260/main.go
tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/ina219/main.go
tinygo build -size short -o ./build/test.hex -target=nucleo-l432kc ./examples/aht20/main.go tinygo build -size short -o ./build/test.hex -target=nucleo-l432kc ./examples/aht20/main.go
tinygo build -size short -o ./build/test.hex -target=feather-m4 ./examples/sdcard/console/ tinygo build -size short -o ./build/test.hex -target=feather-m4 ./examples/sdcard/console/
tinygo build -size short -o ./build/test.hex -target=feather-m4 ./examples/i2csoft/adt7410/ tinygo build -size short -o ./build/test.hex -target=feather-m4 ./examples/i2csoft/adt7410/